Zod Validation Style

Conventions for schema validation with Zod 4. One schema is the source of truth for both the runtime check and the static type; validate at boundaries, infer types from schemas, and prefer the new top-level formats.

Zod 4 is stable: ~14x faster string parsing, ~10x faster to type-check, and a much smaller core bundle than Zod 3. Use it for all new validation. Import from zod (v4 is the default); use the zod/v4 path only while migrating a codebase still pinned to v3. Source: Zod, https://zod.dev/v4, 2026-05-31

Schema is the single source of truth

Define the schema, then derive the type, never maintain a parallel type by hand:

const User = z.object({
  email: z.email(),
  age: z.number().int().nonnegative(),
});
type User = z.infer<typeof User>;

Source: Zod v4 release, 2026-05-31

Prefer top-level string formats

In Zod 4 the string formats are top-level functions, which are more concise and tree-shakable. The method forms (z.string().email(), etc.) are deprecated and will be removed next major:

z.email();  z.url();  z.uuid();  z.iso.datetime();  z.jwt();

Source: Zod v4 release notes, 2026-05-31

Parse at boundaries, with safeParse

  • Validate external input only (request bodies, env vars, third-party responses); do not re-validate trusted internal data.
  • Prefer .safeParse() and map the Result to an HTTP status or UI state (see API Route Style and Error-Handling Style); reserve .parse() (which throws) for startup-time invariants like env parsing.
  • Format failures with the top-level z.treeifyError() (for nested field errors) or z.prettifyError() (for a readable string). The ZodError.format() method is deprecated. Source: Zod v4 migration guide, https://zod.dev/v4/changelog, 2026-05-31

Customize errors with one error param

Zod 4 unifies error customization under a single error parameter, replacing the v3 grab-bag of message, invalid_type_error, and required_error:

z.string({ error: "Name is required" });

Source: Zod v4 migration guide, 2026-05-31

Conventions

  • Co-locate schemas with the boundary they guard (API input schema next to the route, env schema in one env.ts).
  • Reuse and compose with .extend(), .pick(), .omit(), and .merge() instead of redefining shapes.
  • This is the same constrain-the-interface instinct as the Structured Outputs Pipeline: a strict schema makes malformed data unrepresentable rather than something you defensively check downstream.

Boundary Map

Boundary Default parser location Failure handling
API route body/query/params Route-local schema or shared route schema safeParse to 400/422
Server Function input Function-local schema before side effects Typed result or thrown invariant
Env vars Single startup env.ts schema Fail startup
Webhook payload Provider-specific schema plus signature verification Reject before domain processing
Third-party response Adapter parser near SDK boundary Provider error with safe external message
LLM structured output Strict schema beside prompt/eval Retry or fail closed with trace

Proof

Validation code is proven by invalid-input tests, not by the existence of a schema file. For every important schema, include at least one negative case that would have slipped through TypeScript alone. Schema drift should fail at the boundary, not three functions downstream.

Style Rule Contract

This page follows Style Rule Contract: name the default pattern, banned pattern, reason, exception, proof, and codification path clearly enough that the next agent can apply the rule without asking Kevin again. If a local repo has a stricter rule, follow the local rule and update the wiki when the decision becomes durable.


Timeline

  • 2026-07-01 | Aligned this page with Style Rule Contract so style guidance names default behavior, banned patterns, exceptions, proof, and codification expectations. Source: User request, 2026-07-01
  • 2026-07-01 | Added a boundary map and proof requirement for invalid-input tests across API routes, Server Functions, env vars, webhooks, third-party responses, and LLM structured outputs. Source: User request, 2026-07-01
  • 2026-05-31 | Page created from the Zod 4 release notes and migration guide. Captured schema-as-source-of-truth with z.infer, top-level string formats (deprecating .string().email()), safeParse-at-boundaries, z.treeifyError/z.prettifyError (deprecating .format()), and the unified error param. Source: https://zod.dev/v4, 2026-05-31