Skip to content

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 zod

Getting started

The smallest useful thing you can do with it, and what each part means.

One schema, two guarantees
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());
}
parse throws on invalid data. This is the boundary where an unverified payload becomes a value the type system can legitimately trust.
safeParse for expected failures
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 above
Use safeParse when invalid input is a normal outcome, such as a form submission, and parse when it would indicate a bug. flatten() shapes errors for display without manual traversal.

Advanced usage

Where the library earns its place over a simpler alternative.

Transforms, refinements and discriminated unions
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 switch
transform changes the output type as well as the value, so the inferred type is Date rather than string. Setting `path` on a refinement is what makes the error land on the field the user must fix.

Errors 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.