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
parsein hot paths — throws are slow; usesafeParse.- 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 .