Zod
Overview
Zod is a TypeScript-first schema validation library with static type inference. Define a schema once; you get both runtime validation and a TypeScript type (via z.infer). Zero external dependencies; works in Node and browsers. Ideal for forms (e.g. with react-hook-form), API parsing, env vars, and any untrusted input.
Requirements: TypeScript 5.5+ recommended; enable strict in tsconfig.json.
Install: npm install zod
Quick start
import { z } from "zod";
const User = z.object({
name: z.string(),
age: z.number().optional(),
});
type User = z.infer<typeof User>;
// { name: string; age?: number }
const data = User.parse(input); // throws ZodError if invalid
const result = User.safeParse(input); // { success: true, data } | { success: false, error }
Primitives and common types
| Schema | Type | Notes |
|---|---|---|
z.string() | string | |
z.number() | number | |
z.boolean() | boolean | |
z.bigint() | bigint | |
z.date() | Date | |
z.undefined() | undefined | |
z.null() | null | |
z.void() | void | |
z.any() | any | |
z.unknown() | unknown | |
z.never() | never | |
z.literal("x") | literal | |
z.enum(["a", "b"]) | union of literals | |
z.string().email() | string | email format |
z.string().url() | string | URL |
z.string().uuid() | string | UUID |
z.string().datetime() | string | ISO datetime |
z.number().int() | number | integer |
z.number().min(n).max(m) | number | range |
z.boolean() / z.string().transform(...) | stringbool | "true"/"false" coercion |
Optional: .optional() → T | undefined. Nullable: .nullable() → T | null. Nullish: .nullish() → T | null | undefined. Default: .default(value).
Objects
const Schema = z.object({
name: z.string(),
age: z.number().optional(),
tags: z.array(z.string()).default([]),
});
type Schema = z.infer<typeof Schema>;
- .shape:
Schema.shape.name(access field schema). - .keyof():
Schema.keyof()→ enum of keys. - .extend({ ... }): Add or override keys.
- .pick({ name }) / .omit({ age }): Subset of keys.
- .partial() / .required({ name }): Optionalize or require.
- .strict(): No extra keys (default in z.object). z.strictObject / z.looseObject for behavior variants.
- .catchall(z.string()): Allow extra keys with a schema.
- Nested:
z.object({ user: z.object({ name: z.string() }) }). - Recursive:
z.lazy(() => Category)for self-referential structures.
Arrays and tuples
- Arrays:
z.array(z.string()),z.string().array(). - Tuples:
z.tuple([z.string(), z.number()])— fixed length and types. - Non-empty:
.min(1)or dedicated helpers if available.
Unions and intersections
- Union:
z.union([z.string(), z.number()])orz.string().or(z.number()). - Discriminated union: Use a common key (e.g.
type: "a") andz.discriminatedUnion("type", [z.object({ type: z.literal("a"), ... }), ...]). - Intersection:
z.intersection(A, B)orA.and(B).
Refinements and transform
- .refine(fn, message?): Custom validation; keep type unchanged.
- .superRefine: Multiple issues or async; push to
ctx. - .transform(fn): Change output type. Input and output can differ; use
z.input<typeof schema>andz.output<typeof schema>(orz.inferfor output). - .pipe(otherSchema): Chain validation/transform (e.g. string → number via
z.string().pipe(z.coerce.number())).
Parsing and errors
- .parse(input): Returns data or throws ZodError.
- .safeParse(input): Returns
{ success: true, data }or{ success: false, error: ZodError }. - ZodError:
error.issues(array of{ path, message, code }),error.format()for nested shape. - Async:
.parseAsync/.safeParseAsyncfor schemas with async refinements or transforms.
Type inference
- z.infer<typeof schema>: Output type (after transforms/defaults).
- z.input<typeof schema>: Input type (before transforms; useful when input ≠ output).
- z.output<typeof schema>: Same as
z.inferfor output type.
const S = z.string().transform((s) => s.length);
type In = z.input<typeof S>; // string
type Out = z.output<typeof S>; // number
Integration: React Hook Form
Use @hookform/resolvers with zodResolver:
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
const formSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
type FormValues = z.infer<typeof formSchema>;
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: { email: "", password: "" },
});
Best practices
- Prefer strict schemas (no extra keys) for API boundaries; use .passthrough() only when you need to forward unknown keys.
- Use .default() for optional fields with a default value.
- For API/env parsing, use safeParse and handle errors; show user-friendly messages from
error.issues. - Use discriminated unions for variant payloads (e.g. events by
type). - Coerce only when safe:
z.coerce.number(),z.coerce.boolean(), or custom.transform().
Common mistakes
- Forgetting strict: Keep TypeScript
strict: true; Zod works best with it. - Input vs output: After
.transform(), usez.input<>/z.output<>if the type seen by callers differs. - parse in hot path: Prefer validating once at boundaries (e.g. API handler, form submit) rather than on every render.
- Overly broad schema: Prefer specific types (e.g.
.email(),.min()) instead of plainz.string()when the domain has rules.
Additional resources
- reference.md — Official Zod docs links, API sections (primitives, objects, strings, numbers, refinements, etc.), Zod Mini, ecosystem.
- Official: https://zod.dev — Introduction, API, basics, ecosystem.
- API (Zod 4): https://zod.dev/api — Full schema types and methods.
- LLMs / index: https://zod.dev/llms.txt — Structured doc for tools/agents.