Skip to content

Zod Schemas#

Object Schemas#

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  age: z.number().min(0),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'guest']),
});

type User = z.infer<typeof UserSchema>;

Nested Objects#

const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  country: z.string(),
});

const PersonSchema = z.object({
  name: z.string(),
  address: AddressSchema,
});

Union Types#

// Either string or number
z.union([z.string(), z.number()]);

// Shorthand
z.string().or(z.number());

// Discriminated union
const ResponseSchema = z.discriminatedUnion('status', [
  z.object({ status: z.literal('success'), data: z.any() }),
  z.object({ status: z.literal('error'), error: z.string() }),
]);

Transforms#

const StringToNumber = z.string().transform((val) => parseInt(val, 10));

const TrimmedString = z.string().transform((val) => val.trim());

// Chain transforms
const Schema = z.string()
  .transform((val) => val.toLowerCase())
  .transform((val) => val.trim());

Refinements (Custom Validation)#

const PasswordSchema = z.string()
  .min(8)
  .refine((val) => /[A-Z]/.test(val), {
    message: "Must contain uppercase letter",
  })
  .refine((val) => /[0-9]/.test(val), {
    message: "Must contain number",
  });

Partial & Pick#

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
});

// All fields optional
const PartialUser = UserSchema.partial();

// Pick specific fields
const UserName = UserSchema.pick({ name: true });

// Omit fields
const UserWithoutId = UserSchema.omit({ id: true });

Extend#

const BasicUser = z.object({
  name: z.string(),
});

const AdminUser = BasicUser.extend({
  role: z.literal('admin'),
  permissions: z.array(z.string()),
});