Technology handbook
Zod validation libraries
Runtime schemas, TypeScript inference, transformations, errors, forms, API boundaries and schema composition.
- Topics
- 32
- Sections
- 4
Zod foundations
Runtime validation vs TypeScript
Validate external values at runtime instead of relying on a type assertion that performs no check.
- Types are erased at build time and check nothing about a real request body.
- A cast tells the compiler what to assume; a schema tells the program what arrived.
- Parse at every boundary: request bodies, query strings, storage reads and third-party responses.
parse and safeParse
Choose exception-based parsing or a discriminated success result according to the surrounding error flow.
- parse throws on failure, which suits code already wrapped in an error handler.
- safeParse returns a success flag with either the data or the issues, and never throws.
- A route that renders field errors wants the result object, not an exception.
parseAsync and safeParseAsync
Await parsing when refinements or transformations perform asynchronous work.
- Any schema with an async check has to be awaited or it throws at parse time.
- A uniqueness lookup against the database is the usual reason to go async.
- Keep async work out of a schema used on the hot path of a render.
z.infer, z.input and z.output
Derive types from a schema and distinguish accepted input from transformed output.
- One schema is the single definition of both the check and the type.
- Input and output differ as soon as a transform or a default is involved.
- Type a function argument from the input and its result from the output.
Strings and formats
Apply length, pattern and supported format constraints according to the actual domain contract.
- Length and pattern rules belong on the schema, not scattered through handlers.
- Built-in formats cover email, URL, UUID and datetime without hand-written patterns.
- Trim before checking length, or a padded value passes a rule it should fail.
Numbers and coercion
Reject invalid numeric values and test how empty strings or booleans behave under conversion.
- A JSON number arrives typed; a query string or form field never does.
- Coercion accepts anything convertible, so an empty string can become zero.
- Bound the value with min, max and integer rules after converting it.
Booleans, literals and enums
Model a finite allowed set rather than accepting arbitrary strings for a controlled field.
- An enum narrows to exactly the members the code has branches for.
- A literal pins one value, which is what makes a tagged variant work.
- A checkbox posts a string, so a boolean field needs an explicit mapping.
Optional, nullable and nullish
Distinguish an absent value from an explicit null and decide which the API permits.
- Optional allows the key to be absent; nullable allows it to be present and null.
- A patch endpoint needs that distinction to tell skip from clear.
- Nullish accepts both and is right only when the two truly mean the same thing.
Schema composition
Object schemas
Validate a structured record and choose how unknown properties should be handled.
- An object schema validates each known key and reports issues by path.
- Unknown keys are stripped by default, so parsed output carries no extras.
- Nest schemas to mirror the shape of the payload rather than flattening it.
Strict and loose objects
Reject or preserve extra keys deliberately at public request boundaries.
- A strict object rejects unknown keys, which catches a renamed client field early.
- A passthrough object keeps extras, useful when forwarding a payload untouched.
- Strict at a public ingress point; permissive when wrapping a partner response.
Arrays and tuples
Distinguish a repeated element type from a fixed positional contract.
- An array checks every element against one schema and can bound its length.
- A tuple fixes the length and gives each position its own schema.
- Validating a large array on every request is a real cost worth measuring.
Records, maps and sets
Model keyed collections and unique collections with validation of their contained values.
- A record validates keys and values when the key names are not known ahead of time.
- Map and Set schemas check the collection type itself, not just its contents.
- A record with an unbounded key space is a place to impose a size limit.
Unions
Accept one of several explicit input shapes and provide meaningful errors for unmatched input.
- A union tries each member and fails only when none matches.
- The error lists every branch, which reads poorly for more than a few members.
- Order matters when members overlap, because the first match wins.
Discriminated unions
Use a literal tag to select a variant and simplify downstream type narrowing.
- A shared literal key selects one branch instead of trying them all.
- Errors come from the matched variant, so the message points at the real problem.
- Parsing stays fast because only one member is ever evaluated.
Intersections
Combine compatible requirements while understanding how the resulting values are merged.
- An intersection requires a value to satisfy both schemas at once.
- Conflicting requirements make a schema nothing can satisfy, with no warning.
- Extending an object is usually clearer than intersecting two of them.
pick, omit and partial
Derive related object contracts without accidentally weakening required production checks.
- Derive a narrower contract from the canonical schema rather than restating it.
- A partial schema suits a patch body where every field is genuinely optional.
- Partial applied to a create schema quietly drops the checks that mattered.
extend and schema reuse
Compose fields from shared contracts and avoid incompatible overrides.
- Extend adds fields and can also replace one, which silently relaxes it.
- Keep shared field definitions in one place so a rule changes once.
- A base schema plus per-endpoint extensions beats one schema with everything optional.
Recursive schemas
Describe nested structures with explicit recursion and realistic size or depth limits.
- A self-referencing schema needs a lazy wrapper or the definition cannot close.
- Annotate the type by hand, since inference cannot resolve the cycle alone.
- Cap the depth, or a deliberately nested payload becomes a denial of service.
Validation integration
Error formatting
Turn structured issues into field and form errors without discarding nested paths.
- Every issue carries a path, so a nested failure can reach the right input.
- Flatten for a simple form; walk the tree when fields are nested or repeated.
- Collect all issues rather than showing the user one mistake at a time.
Custom error messages
Give precise corrective feedback while avoiding disclosure of sensitive internal constraints.
- A message written for the field beats a generic type complaint.
- Say what is required, not what the internal rule is called.
- Do not leak limits or internal names that reveal how the backend is built.
FormData validation
Handle strings, repeated fields and files before assuming browser form values match domain types.
- Every form value arrives as a string, including numbers and checkboxes.
- A repeated field name yields several entries that must be collected into an array.
- A file input needs size and type checks before anything touches storage.
API request and response schemas
Validate ingress and assert egress contracts without exposing private database fields.
- Validate ingress so a handler never works on an unchecked shape.
- Parse the response too, so an internal field cannot leak into the payload.
- Sharing the schema with the client keeps both ends on one contract.
Environment validation
Fail early on missing server settings while preventing secrets from entering public client configuration.
- Check configuration once at startup rather than at first use.
- A missing setting should stop the process, not surface as a runtime failure later.
- Keep server settings in a schema the client bundle never imports.
Schema testing and migration
Test valid, invalid and boundary cases before changing a contract used by existing clients.
- Test the boundaries: empty, maximum length, wrong type and extra keys.
- Tightening a live schema rejects payloads that used to work, so stage it.
- Accept both shapes during a migration window, then drop the old one.
Sources
- Zod validation libraries official documentation