Click any item to expand the explanation and examples.
📦 Primitives
Basic types basics
import { z } from 'zod';
z.string()
z.number()
z.bigint()
z.boolean()
z.date()
z.undefined()
z.null()
z.void()
z.any()
z.unknown()
z.never()
// Literals
z.literal(‘hello’)
z.literal(42)
z.literal(true)
String validations string
z.string().min(1) // Non-empty
z.string().min(3, 'Too short') // Custom message
z.string().max(255)
z.string().length(10)
z.string().email()
z.string().url()
z.string().uuid()
z.string().cuid()
z.string().regex(/^[a-z]+$/)
z.string().startsWith('https://')
z.string().endsWith('.com')
z.string().trim() // Trims whitespace
z.string().toLowerCase()
z.string().toUpperCase()
z.string().datetime() // ISO 8601
z.string().ip() // IPv4 or IPv6
Number validations number
z.number().int() z.number().positive() z.number().nonnegative() z.number().negative() z.number().min(0) z.number().max(100) z.number().multipleOf(5) z.number().finite() z.number().safe() // Number.MIN_SAFE_INTEGER to MAX
📋 Objects
Object schemas object
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().int().positive(),
});
// Parse (throws on error)
const user = UserSchema.parse(data);
// Safe parse (returns result object)
const result = UserSchema.safeParse(data);
if (result.success) {
console.log(result.data);
} else {
console.log(result.error.issues);
}
// Infer TypeScript type
type User = z.infer<typeof UserSchema>;
// { name: string; email: string; age: number }
Optional, nullable, defaults object
z.object({
name: z.string(),
bio: z.string().optional(), // string | undefined
avatar: z.string().nullable(), // string | null
role: z.string().default('user'), // defaults to 'user'
tags: z.array(z.string()).default([]),
});
// .nullish() = optional + nullable (string | null | undefined)
Object manipulation object
const User = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
password: z.string(),
});
// Pick specific fields
const PublicUser = User.pick({ name: true, email: true });
// Omit fields
const CreateUser = User.omit({ id: true });
// Make all fields optional
const UpdateUser = User.partial();
// Make specific fields optional
const PatchUser = User.partial({ name: true, email: true });
// Extend
const AdminUser = User.extend({
role: z.literal(‘admin’),
permissions: z.array(z.string()),
});
// Merge two schemas
const Combined = SchemaA.merge(SchemaB);
// Strip unknown keys (default behavior)
User.parse(dataWithExtraKeys); // extra keys removed
// Allow unknown keys
User.passthrough().parse(data); // extra keys kept
// Reject unknown keys
User.strict().parse(data); // throws if extra keys
📚 Arrays, Tuples, Unions
Arrays collection
z.array(z.string()) // string[] z.array(z.number()).min(1) // At least 1 item z.array(z.number()).max(10) // At most 10 z.array(z.number()).length(3) // Exactly 3 z.array(z.number()).nonempty() // At least 1 (narrows type)// Shorthand z.string().array() // Same as z.array(z.string())
Unions, enums, discriminated unions collection
// Union z.union([z.string(), z.number()]) z.string().or(z.number()) // Shorthand// Enum z.enum([‘admin’, ‘user’, ‘guest’]) const RoleEnum = z.enum([‘admin’, ‘user’, ‘guest’]); type Role = z.infer<typeof RoleEnum>; // ‘admin’ | ‘user’ | ‘guest’ RoleEnum.enum.admin // ‘admin’ (autocomplete!)
// Native enum enum Direction { Up, Down } z.nativeEnum(Direction)
// Discriminated union (better error messages) const Shape = z.discriminatedUnion(‘type’, [ z.object({ type: z.literal(‘circle’), radius: z.number() }), z.object({ type: z.literal(‘rect’), width: z.number(), height: z.number() }), ]);
// Tuple z.tuple([z.string(), z.number()]) // [string, number]
// Record z.record(z.string(), z.number()) // { [key: string]: number }
🔄 Transforms & Pipes
Transform, refine, preprocess transform
// Transform output const trimmed = z.string().transform(s => s.trim()); const toNumber = z.string().transform(Number);// Coerce (parse input to type) z.coerce.number() // “42” → 42 z.coerce.boolean() // “true” → true z.coerce.date() // “2026-01-01” → Date z.coerce.string() // 42 → “42”
// Custom validation with refine const password = z.string() .min(8) .refine(s => /[A-Z]/.test(s), ‘Needs uppercase’) .refine(s => /[0-9]/.test(s), ‘Needs number’);
// Superrefine (multiple errors) const schema = z.object({ password: z.string(), confirm: z.string(), }).superRefine((data, ctx) => { if (data.password !== data.confirm) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: ‘Passwords must match’, path: [‘confirm’], }); } });
// Preprocess (transform before validation) z.preprocess( (val) => (typeof val === ‘string’ ? val.trim() : val), z.string().min(1) );
⚡ Common Patterns
API request/response validation pattern
// Define schemas
const CreateUserInput = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
password: z.string().min(8),
});
const UserResponse = z.object({
id: z.number(),
email: z.string(),
name: z.string(),
createdAt: z.coerce.date(),
});
// Use in API route
export async function POST(req: Request) {
const body = await req.json();
const input = CreateUserInput.parse(body);
// input is fully typed: { email: string, name: string, password: string }
const user = await createUser(input);
return Response.json(UserResponse.parse(user));
}
Environment variable validation pattern
const envSchema = z.object({
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});
export const env = envSchema.parse(process.env);
Validates all env vars at startup. If anything is missing, you get a clear error instead of a runtime crash later.
Form validation pattern
const ContactForm = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
message: z.string().min(10, 'Message too short').max(1000),
});
// Get formatted errors
const result = ContactForm.safeParse(formData);
if (!result.success) {
const errors = result.error.flatten();
// errors.fieldErrors = { name: [’…’], email: [’…’] }
}