All topics
Frontend · Learning hub

Zod notes for developers

Master Zod with a curated set of 1 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Frontend notes
Zod

Zod Validation

Zod Validation Schema Basics import { z } from 'zod'; // Primitives z.string() z.number() z.boolean() z.date() z.undefined() z.null() z.unknown() z.any() z.void

Zod Validation

Schema Basics

import { z } from 'zod';

// Primitives
z.string()
z.number()
z.boolean()
z.date()
z.undefined()
z.null()
z.unknown()
z.any()
z.void()
z.never()

// String refinements
z.string().min(1).max(255)
z.string().email()
z.string().url()
z.string().uuid()
z.string().regex(/^[a-z]+$/)
z.string().startsWith('https://')
z.string().trim()               // transforms: trims whitespace
z.string().toLowerCase()        // transforms: lowercases
z.string().optional()           // string | undefined
z.string().nullable()           // string | null
z.string().nullish()            // string | null | undefined
z.string().default('anonymous') // provides default value

// Number refinements
z.number().int()
z.number().positive()
z.number().min(0).max(100)
z.number().finite()
z.number().safe()               // within Number.MIN/MAX_SAFE_INTEGER

// Literals & enums
z.literal('hello')
z.literal(42)
z.enum(['pending', 'active', 'inactive'])
z.nativeEnum(MyTsEnum)

// Arrays
z.array(z.string())
z.array(z.string()).min(1).max(10)
z.array(z.string()).nonempty()
z.tuple([z.string(), z.number()])  // fixed-length typed tuple

Objects & Composition

// Object schema
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100),
  email: z.string().email().toLowerCase(),
  age: z.number().int().min(0).max(150).optional(),
  role: z.enum(['admin', 'user']).default('user'),
  createdAt: z.date().or(z.string().transform(s => new Date(s))),
  settings: z.object({
    theme: z.enum(['light', 'dark']).default('light'),
    notifications: z.boolean().default(true),
  }).optional(),
});

type User = z.infer<typeof UserSchema>;  // TypeScript type from schema

// Object methods
UserSchema.partial()                     // all fields optional
UserSchema.required()                    // all fields required
UserSchema.pick({ id: true, name: true })
UserSchema.omit({ createdAt: true })
UserSchema.extend({ newField: z.string() })
UserSchema.merge(OtherSchema)
UserSchema.strip()   // remove unknown keys (default)
UserSchema.passthrough()  // allow unknown keys
UserSchema.strict()  // throw on unknown keys

// Union & intersection
const StringOrNumber = z.union([z.string(), z.number()]);
const StringOrNumber2 = z.string().or(z.number());  // shorthand

const AdminUser = UserSchema.and(z.object({ permissions: z.array(z.string()) }));

// Discriminated union
const EventSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
  z.object({ type: z.literal('keypress'), key: z.string() }),
]);

// Record (key-value map)
const Config = z.record(z.string(), z.unknown());
const NameMap = z.record(z.string(), z.string());  // { [key: string]: string }

Parsing & Error Handling

// parse — throws ZodError on invalid data
const user = UserSchema.parse(rawData);

// safeParse — returns { success, data } or { success, error }
const result = UserSchema.safeParse(rawData);
if (result.success) {
  console.log(result.data.email);   // fully typed
} else {
  console.log(result.error.issues); // ZodIssue[]
  console.log(result.error.format()); // nested error object
  console.log(result.error.flatten()); // { formErrors: [], fieldErrors: {} }
}

// Custom error messages
z.string({ required_error: 'Name is required' })
  .min(1, 'Name cannot be empty')
  .max(100, { message: 'Name too long' });

// Custom validation with refine
const PasswordSchema = z.object({
  password: z.string().min(8),
  confirm: z.string(),
}).refine(data => data.password === data.confirm, {
  message: 'Passwords do not match',
  path: ['confirm'],   // which field to attach error to
});

// superRefine — multiple errors
const Schema = z.string().superRefine((val, ctx) => {
  if (!/[A-Z]/.test(val)) {
    ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Need uppercase' });
  }
  if (!/[0-9]/.test(val)) {
    ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Need digit' });
  }
});

// Transform — validate then transform
const MoneySchema = z.string()
  .regex(/^\d+(\.\d{2})?$/, 'Invalid amount')
  .transform(s => parseFloat(s));
// MoneySchema.parse('10.50') → 10.5 (number)

Keep your Zod knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever