Zod cheatsheet.

Install

npm i zod

Basic schema

import { z } from "zod";

const User = z.object({
  id: z.number().int().positive(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().nonnegative().optional(),
  role: z.enum(["admin", "user"]),
  createdAt: z.date(),
});

type User = z.infer<typeof User>;

parse vs safeParse

// parse: throws ZodError on invalid
const user = User.parse(input);

// safeParse: returns Result
const r = User.safeParse(input);
if (r.success) {
  r.data;       // User
} else {
  r.error;      // ZodError
}

Primitives

z.string()
z.string().min(1).max(50)
z.string().email().url().regex(/^[a-z]+$/).startsWith("/")
z.string().uuid()
z.string().trim().toLowerCase()

z.number().int().positive().min(0).max(100)
z.number().finite().safe()

z.boolean()
z.bigint()
z.date()
z.literal("admin")
z.enum(["a", "b", "c"])
z.nativeEnum(MyEnum)

z.null()
z.undefined()
z.void()
z.any()
z.unknown()
z.never()

Optional / nullable

z.string().optional()              // T | undefined
z.string().nullable()              // T | null
z.string().nullish()               // T | null | undefined
z.string().default("hi")           // T (replaces undefined)

Composite

z.array(z.string()).min(1).max(10)
z.tuple([z.string(), z.number()])
z.set(z.number())
z.map(z.string(), z.number())
z.record(z.string())               // Record<string, string>
z.record(z.string(), z.number())   // Record<string, number>

Unions / discriminated unions

const Animal = z.union([
  z.object({ kind: z.literal("dog"), bark: z.string() }),
  z.object({ kind: z.literal("cat"), meow: z.string() }),
]);

// Faster + better errors:
const Animal = z.discriminatedUnion("kind", [
  z.object({ kind: z.literal("dog"), bark: z.string() }),
  z.object({ kind: z.literal("cat"), meow: z.string() }),
]);

Intersections

const A = z.object({ a: z.string() });
const B = z.object({ b: z.number() });
const AB = z.intersection(A, B);    // { a: string; b: number }

Extend / merge / pick / omit

const User = z.object({ id: z.number(), name: z.string() });

const Admin = User.extend({ role: z.literal("admin") });
const Merged = User.merge(z.object({ extra: z.string() }));
const Public = User.pick({ name: true });
const NoId = User.omit({ id: true });
const PartialUser = User.partial();
const RequiredUser = User.required();
const DeepPartial = User.deepPartial();

Refine (custom validation)

const Password = z.string().min(8).refine(
  (s) => /[A-Z]/.test(s),
  { message: "must have uppercase" },
);

const Range = z.object({ from: z.number(), to: z.number() })
  .refine((r) => r.from < r.to, { message: "from must be < to" });

superRefine (multiple issues)

const Schema = z.object({ a: z.number(), b: z.number() }).superRefine((v, ctx) => {
  if (v.a < 0) ctx.addIssue({ code: "custom", path: ["a"], message: "negative" });
  if (v.b < 0) ctx.addIssue({ code: "custom", path: ["b"], message: "negative" });
});

Transform

const StringToNumber = z.string().transform((s) => parseInt(s, 10));
// Input: string, Output: number

const Trimmed = z.string().transform((s) => s.trim());

const User = z.object({
  name: z.string().transform((s) => s.toLowerCase()),
});

pipe

const NumString = z.string().pipe(
  z.coerce.number().int().positive()
);

NumString.parse("42");     // 42 (number)

coerce

z.coerce.number().parse("42")        // 42
z.coerce.string().parse(42)          // "42"
z.coerce.boolean().parse("true")     // true (any truthy)
z.coerce.date().parse("2026-01-01")  // Date

Brand

const Email = z.string().email().brand<"Email">();
type Email = z.infer<typeof Email>;

function send(to: Email) { ... }

const e = Email.parse("[email protected]");
send(e);

API boundary pattern

const ApiResponse = z.object({
  data: z.object({ id: z.number(), name: z.string() }),
  meta: z.object({ total: z.number() }),
});

async function fetchUser(): Promise<z.infer<typeof ApiResponse>> {
  const res = await fetch("/api/user");
  const json = await res.json();
  return ApiResponse.parse(json);
}

Validate at the edge; internal code can trust the types.

Error formatting

const r = User.safeParse(input);
if (!r.success) {
  r.error.issues;            // ZodIssue[]
  r.error.flatten();         // { formErrors, fieldErrors }
  r.error.format();          // nested tree
}

With express / fastify / Next

function validate<T>(schema: z.ZodSchema<T>) {
  return (req: Request, res: Response, next: NextFunction) => {
    const r = schema.safeParse(req.body);
    if (!r.success) return res.status(400).json(r.error.flatten());
    req.body = r.data;
    next();
  };
}

app.post("/users", validate(User), handler);

Common mistakes

  • parse in hot paths — throws are slow; use safeParse.
  • Validating internal data — boundary only; internal trust the types.
  • Bare z.object({}) accepts unknown keys silently; use .strict() to reject.
  • Forgetting .brand() — branded type isn’t applied automatically.
  • Forgetting to call z.infer<typeof Schema> — passing the schema where the inferred type is expected.

Read this next

If you want my Zod patterns + API helpers, they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .