What it is
Zod is a TypeScript-first schema declaration and validation library. One schema produces both the runtime check and the static type, so the two can never drift apart.
Define a schema with the z builder, then parse untrusted data through it. z.infer extracts the static type, so the validator is the single source of truth for both worlds.
Installation
npm install zodGetting started
The smallest useful thing you can do with it, and what each part means.
import { z } from 'zod';
const UserSchema = z.object({
id: z.number().int().positive(),
email: z.string().email(),
role: z.enum(['admin', 'member']),
bio: z.string().max(500).optional(),
});
// The type is derived — it cannot fall out of sync with the check.
type User = z.infer<typeof UserSchema>;
async function loadUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
// response.json() is `any`; parse turns it into something typed and verified.
return UserSchema.parse(await response.json());
}const result = UserSchema.safeParse(formData);
if (!result.success) {
// Field-keyed errors, ready to render next to inputs.
const errors = result.error.flatten().fieldErrors;
return { errors };
}
const user = result.data; // fully typed, narrowed by the check aboveAdvanced usage
Where the library earns its place over a simpler alternative.
const DateFromString = z.string().datetime().transform((s) => new Date(s));
const PasswordChange = z
.object({ password: z.string().min(12), confirm: z.string() })
.refine((data) => data.password === data.confirm, {
message: 'Passwords must match',
path: ['confirm'], // attaches the error to the right field
});
// Discriminated unions parse faster and give far better error messages
// than a plain union, because Zod only tries the matching branch.
const Event = z.discriminatedUnion('type', [
z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
z.object({ type: z.literal('key'), key: z.string() }),
]);
type Event = z.infer<typeof Event>; // narrows correctly in a switchErrors and fixes
The failures you are most likely to hit, and what actually resolves them.
- ZodError with a deeply nested issues array
- Use error.flatten() for form display or error.format() for a nested shape mirroring the input.
- Unknown keys are silently dropped
- Zod strips unrecognised keys by default. Use .strict() to reject them or .passthrough() to keep them.
Best practices
- Validate at every boundary: HTTP responses, form input, environment variables, message payloads.
- Derive types with z.infer rather than declaring an interface alongside the schema.
- Use safeParse for user input and parse for invariants that should never fail.
- Prefer z.discriminatedUnion over z.union when there is a tag field — it is faster and the errors are usable.
Background
Why it exists, and what it was reacting to.
Created by Colin McDonnell, Zod solved the fundamental gap in TypeScript: types vanish at compile time, so data arriving from a network or a form is unchecked no matter how precisely it is typed. Its z.infer pattern is now the standard idiom for typing I/O boundaries.
