Zod logoZodv4INTERMEDIATE

Zod

Complete reference for Zod — the TypeScript-first schema validation library with static type inference

8 min read
zodtypescriptvalidationschemaparsingtype-safetyforms
Loading your progress

Installation & Setup

Install Zod and start defining schemas

Install Zod and import it into your project

typescript
# Install Zod
npm install zod
bun add zod
pnpm add zod

// import.ts
import * as z from "zod";

const schema = z.string();
schema.parse("hello"); // "hello"
💡 Enable "strict": true in tsconfig.json for full type inference benefits
⚡ Zod has zero dependencies and works in Node, Bun, Deno, and the browser
📌 Use import * as z for the recommended namespace import pattern
🟢 One schema gives you both runtime validation AND a TypeScript type
installsetup

Primitive Schemas

Built-in schemas for JavaScript primitive types

Primitive Types

Schemas for strings, numbers, booleans, dates, and special types

typescript
import * as z from "zod";

z.string();
z.number();
z.boolean();
z.date();
z.bigint();
z.symbol();

z.null();
z.undefined();
z.any();
z.unknown();
z.never();
💡 Zod schemas are immutable — methods return new schemas instead of mutating
⚡ Use z.unknown() instead of z.any() to keep type safety throughout your code
📌 z.literal() matches an exact value — useful for tagged unions and constants
🟢 z.date() validates Date objects, not date strings — use z.coerce.date() for strings
primitivestypes

String Validation

String-specific validators and format checks

Length, format, and pattern validators for strings

typescript
z.string().min(3);
z.string().max(20);
z.string().length(10);
z.email();
z.url();
z.uuid();
z.string().regex(/^[a-z]+$/);
💡 In Zod 4, format validators like z.email() are top-level functions, not z.string().email()
⚡ Chain string methods to build complex validators — they all return ZodString
📌 Use z.iso.datetime() for ISO 8601 strings and z.coerce.date() to convert to Date objects
🟢 String transformations like .trim() and .toLowerCase() apply during parsing
stringvalidationemailurl

Number Validation

Number-specific validators for ranges and integer constraints

Range, integer, and sign validators for numbers

typescript
z.number().min(0);
z.number().max(100);
z.number().int();
z.number().positive();
z.number().negative();
z.number().multipleOf(5);
💡 z.number() rejects NaN by default — use z.nan() if you need to allow it
⚡ Use .int() before other validators for cleaner integer-only schemas
📌 .multipleOf() works for floats like 0.01 — perfect for currency validation
🟢 .safe() ensures the number fits in JavaScript safe integer range
numbervalidationinteger

Object Schemas

Define and manipulate object shapes with z.object()

Create object schemas with required and optional properties

typescript
const UserSchema = z.object({
  id: z.uuid(),
  name: z.string(),
  email: z.email(),
  age: z.number().optional(),
  createdAt: z.date(),
});

type User = z.infer<typeof UserSchema>;
💡 z.object() strips unknown keys by default — use strictObject to reject them
⚡ z.infer extracts a TypeScript type from a schema — single source of truth
📌 Optional properties become T | undefined in the inferred type
🟢 Use catchall() to validate dynamic keys against a fallback schema
objectschema

Object Methods

Transform existing object schemas with extend, pick, omit, partial

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

const PublicUser = UserSchema.omit({ email: true });
const UserUpdate = UserSchema.partial();
const AdminUser = UserSchema.extend({ role: z.string() });
💡 .pick() and .omit() take an object with true values, not an array of keys
⚡ .partial() is perfect for PATCH update endpoints where any field is optional
📌 .extend() takes priority over the original — duplicate keys are overridden
🟢 Use .keyof() to get an enum of all valid keys for type-safe field references
objectpickomitextend

Arrays, Tuples, Sets & Maps

Schemas for collection types

Define arrays, tuples, sets, maps, and records

typescript
// Array
z.array(z.string());
z.string().array();

// Tuple (fixed length, mixed types)
z.tuple([z.string(), z.number(), z.boolean()]);

// Record (key-value object)
z.record(z.string(), z.number());
💡 z.array(z.X) and z.X.array() are equivalent — pick whichever reads better
⚡ Use z.tuple() for fixed-length arrays with positional types like coordinates
📌 z.record() validates plain objects with dynamic keys — different from z.map()
🟢 z.set() and z.map() work on actual Set/Map instances, not arrays/objects
arraytuplesetmaprecord

Unions, Intersections & Discriminated Unions

Combine multiple schemas with unions and intersections

Union, intersection, and discriminated union patterns

typescript
// Union (any of)
const StringOrNumber = z.union([z.string(), z.number()]);
// or: z.string().or(z.number())

// Discriminated union (faster, better errors)
const Shape = z.discriminatedUnion("kind", [
  z.object({ kind: z.literal("circle"), radius: z.number() }),
  z.object({ kind: z.literal("square"), size: z.number() }),
]);
💡 Use discriminatedUnion when objects share a common literal field — much faster than union
⚡ Discriminated unions give clearer error messages by checking only the matching variant
📌 Prefer .extend() over intersection for combining object schemas — same result, cleaner
🟢 z.union() tries each schema in order — first match wins
unionintersectiondiscriminated

Enums & Literals

Define schemas for fixed sets of values

Restrict values to a known set with z.enum() and z.literal()

typescript
// Literal (exact value)
const Tuna = z.literal("tuna");

// String enum
const Fish = z.enum(["tuna", "salmon", "trout"]);

// Native TypeScript enum
enum Status { ACTIVE = "ACTIVE", INACTIVE = "INACTIVE" }
const StatusSchema = z.nativeEnum(Status);
💡 z.enum() takes a tuple of strings — much better DX than z.nativeEnum()
⚡ Use as const with z.enum() to share the values as both runtime and types
📌 z.literal() is perfect for discriminator fields in discriminated unions
🟢 Access .options on a z.enum to get the array of valid values at runtime
enumliteral

Optional, Nullable, Default & Catch

Handle missing values, defaults, and fallbacks

Optional, nullable, default values, and catch fallbacks

typescript
z.string().optional();   // string | undefined
z.string().nullable();   // string | null
z.string().nullish();    // string | null | undefined
z.string().default("hello");
z.string().catch("fallback");
💡 .default() only kicks in when the value is undefined, not null
⚡ .catch() is like a try/catch — it swallows ALL errors and returns the fallback
📌 Use .nullish() for fields that may be null OR undefined (common in databases)
🟢 .default() with a function generates a fresh value on every parse — ideal for IDs
optionalnullabledefault

Parsing & Error Handling

Parse data and handle validation errors

Throwing and non-throwing parse methods

typescript
// parse - throws on error
try {
  const user = UserSchema.parse(data);
} catch (err) {
  if (err instanceof z.ZodError) {
    console.log(err.issues);
  }
}

// safeParse - returns result object
const result = UserSchema.safeParse(data);
if (result.success) {
  console.log(result.data);
} else {
  console.log(result.error.issues);
}
💡 Prefer safeParse() in API routes — no try/catch, cleaner control flow
⚡ z.flattenError() is perfect for form validation — gives field-level error arrays
📌 Use parseAsync() when your schema has async refinements like database lookups
🟢 ZodError.issues is an array — each issue has code, path, message for granular handling
parsesafeParseerrors

Set custom error messages on schemas and validators

typescript
z.string({ message: "Name is required" });
z.string().min(3, "Too short");
z.email({ message: "Invalid email" });

// Per-validator
z.string()
  .min(3, "At least 3 characters")
  .max(20, "At most 20 characters")
  .regex(/^[a-z]+$/, "Lowercase only");
💡 In Zod 4, error messages use the { message: "..." } params object pattern
⚡ Use function-based messages to access the invalid value in your error string
📌 z.config() sets a global error handler — useful for i18n and consistent messaging
🟢 Per-validator messages are more specific than top-level — order matters
errorsmessagesvalidation

Refinements & Transforms

Custom validation logic and data transformations

Add custom validation logic to any schema

typescript
// Simple refinement
const PositiveInt = z.number().refine(
  (val) => Number.isInteger(val) && val > 0,
  { message: "Must be a positive integer" }
);

// Object-level refinement
const Form = z.object({
  password: z.string().min(8),
  confirm: z.string(),
}).refine((data) => data.password === data.confirm, {
  message: "Passwords don't match",
  path: ["confirm"],
});
💡 Use refine() for single conditions, superRefine() when you need multiple errors
⚡ Cross-field validation (like password confirm) requires object-level refine()
📌 Set { path: ["field"] } to attach the error to a specific field instead of the root
🟢 Async refinements MUST be used with parseAsync — sync parse will throw
refinevalidationcustom

Convert and transform data during validation

typescript
// Transform output
z.string().transform((val) => val.toUpperCase());

// Chain with pipe
z.string()
  .transform((val) => parseInt(val))
  .pipe(z.number().int().positive());

// Coerce input to type
z.coerce.number().parse("42"); // 42
z.coerce.date().parse("2024-01-01");
💡 z.coerce is a shortcut for the most common preprocess pattern — type coercion
⚡ Use coerce for query params and form data where everything starts as strings
📌 .pipe() chains a transform into another schema for additional validation
🟢 Input and output types differ when transforms change the type — use z.input/z.output
transformcoercepipe

Type Inference

Extract TypeScript types from Zod schemas

Use schemas as the single source of truth for runtime and types

typescript
const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
});

type User = z.infer<typeof UserSchema>;
// { name: string; age: number }
💡 z.infer gives you a TypeScript type for free — no need to write interfaces twice
⚡ Use z.input for function parameters and z.output for return types when transforms are involved
📌 Schemas are the single source of truth — change once, types update everywhere
🟢 Pair Zod with @hookform/resolvers/zod for fully typed React Hook Form validation
infertypesinference