Zod logoZod INTERMEDIATE

Zod

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

8 min read
zodtypescriptvalidationschemaparsingtype-safetyforms

Installation & Setup

Install Zod and start defining schemas

Install & Import

Install Zod and import it into your project

typescript
💡 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
💡 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

String Validators

Length, format, and pattern validators for strings

typescript
💡 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

Number Validators

Range, integer, and sign validators for numbers

typescript
💡 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()

Defining Objects

Create object schemas with required and optional properties

typescript
💡 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
💡 .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

Collection Schemas

Define arrays, tuples, sets, maps, and records

typescript
💡 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

Combining Schemas

Union, intersection, and discriminated union patterns

typescript
💡 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

Enum & Literal Schemas

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

typescript
💡 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

Handling Missing Values

Optional, nullable, default values, and catch fallbacks

typescript
💡 .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

parse() vs safeParse()

Throwing and non-throwing parse methods

typescript
💡 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

Customizing Error Messages

Set custom error messages on schemas and validators

typescript
💡 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

refine() & superRefine()

Add custom validation logic to any schema

typescript
💡 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

transform(), pipe() & coerce

Convert and transform data during validation

typescript
💡 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

z.infer, z.input & z.output

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

typescript
💡 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