Zod

repository·main·Indexed 12 days ago

https://github.com/colinhacks/zod

A TypeScript-first schema declaration and validation library that allows developers to define data schemas once and automatically infer static TypeScript types. It features a functional 'parse, don't validate' approach, zero dependencies, and is environment agnostic. Key capabilities include object schema transformations (.extend, .merge, .pick, .omit), union and discriminated union support, and comprehensive error handling via ZodError, ZodIssue, and custom ZodErrorMap.

Tokens
96.6K
Snippets
358
Records
424
Agent score
97%

What's inside Zod

  1. What is Zod?

    main

    Zod is a TypeScript-first schema declaration and validation library. It allows you to define a schema (representing any data type from a simple string to a complex nested object) once, and then automatically infer the static TypeScript type from it. This eliminates the need for duplicative type declarations.

    Key features include:

    • Zero dependencies: Lightweight and secure.
    • Environment agnostic: Works in Node.js and all modern browsers.
    • Tiny footprint: Approximately 8kb minified + zipped.
    • Immutable: Methods like .optional() return a new instance rather than mutating the original.
    • Functional approach: Follows the "parse, don't validate" philosophy.
    • JavaScript compatible: Can be used in plain JavaScript projects without TypeScript.
  2. What is Zod

    main

    Zod is a schema declaration and validation library that aims to provide a schema API mapping one-to-one to TypeScript's type system. It uses a concise, chainable API to define complex types, making it autocomplete-friendly and developer-friendly. For applications with extremely strict bundle size constraints, Zod Mini is an alternative.

    import * as z from "zod";
    
    const schema = z.object({
      name: z.string(),
      age: z.number().int().positive(),
      email: z.email(),
    });
  3. Explore the Zod Ecosystem

    main

    Zod has a vast ecosystem of tools that extend its functionality. These tools are categorized by how they interact with Zod schemas:

    API Libraries

    Tools for building type-safe APIs and servers, such as tRPC, zodios, express-zod-api, and oRPC.

    Form Integrations

    Libraries that connect Zod schemas to UI form state management, including react-hook-form (via resolvers), TanStack Form, conform, and sveltekit-superforms.

    Schema Transformations

    • Zod to X: Convert Zod schemas into other formats like TypeScript definitions (zod-to-ts), JSON Schemas (zod-to-json-schema), or OpenAPI documentation (zod-to-openapi).
    • X to Zod: Generate Zod schemas from existing definitions like TypeScript (ts-to-zod), JSON Schemas (json-schema-to-zod), or database schemas like Prisma (zod-prisma).

    Mocking and Testing

    Generate mock data or test fixtures from your schemas using tools like @anatine/zod-mock, zod-fixture, or zocker.

    Utilities

    General purpose helpers such as zod-playground for testing schemas or zod-dev for conditionally disabling runtime parsing in production.

  4. Use Zod Checks for post-parsing refinements

    main

    Checks are refinements that run after parsing. They do not affect the inferred type. Every schema contains an array of checks accessible via schema._zod.def.checks.

    All first-party checks are exported as the union z.$ZodChecks. You can discriminate between them using the ._zod.def.check property (e.g., "less_than", "string_format").

    Note: Some string format checks (like z.email()) implement both $ZodCheck and $ZodType. They can be used as a standalone type or as a refinement via .check().

    // As a type
    z.email().parse("user@example.com");
    
    // As a check
    z.string().check(z.email()).parse("user@example.com");
  5. How `optout` controls output presence

    main

    The optout signal (value: "optional" or undefined) determines if a schema's output can be undefined even when the input was present. If optout is set to "optional", parent containers like $ZodObject or $ZodTuple may treat an undefined output as 'absent' (e.g., omitting the key from the final object or trimming a trailing tuple slot).

    Schemas that set optout to "optional":

    • $ZodOptional
    • $ZodExactOptional

    The Four Combinations of Optionality

    Because optin and optout are independent, you can encounter these patterns:

    • Input-required, Output-required: Standard behavior.
    • Input-required, Output-optional: e.g., z.string().nullable() (if the logic allows).
    • Input-optional, Output-required: e.g., z.string().default("d") (accepts absence, but always produces a value).
    • Input-optional, Output-optional: e.g., z.string().optional().
  6. Generate mock data from Zod schemas

    main

    For testing purposes, you can use several libraries to generate mock data based on your Zod schemas:

    • @anatine/zod-mock: Generates mock data using faker.js.
    • zod-fixture: Automatically generates deterministic test fixtures from Zod schemas.
    • zocker: Creates realistic mock data based on your schema.
    • zod-schema-faker: Uses @faker-js/faker and randexp.js to generate mock data.
  7. Understand Optionality and Absence Handling in Zod v4

    main

    In Zod v4, schemas handle missing or undefined input through a distinction between static types and runtime behavior. This is known as the "flexible inputs, strict outputs" pattern.

    Core Mental Model

    • Static Types are Strict: z.input<typeof schema> will show a field as required if the schema uses .catch(), .preprocess(), or .transform(). This tells TypeScript users they should provide the value.
    • Runtime is Flexible: At runtime, these same schemas may accept undefined to trigger recovery logic (like .catch()) or transformation functions.
    • The optin Flag: A schema's optin property determines if an object or tuple parser will allow a key to be missing. If optin is undefined, the parser rejects absent keys.
    • The fallback Flag: When a schema (like .catch() or .transform()) produces a value in response to undefined, it marks that value with a fallback flag. This allows an outer .optional() wrapper to decide whether to keep that value or override it with undefined.

    Summary of Schema Behaviors

    Schema Typeoptin valueBehavior on undefined / Absent Key
    z.string(), z.number(), etc.undefinedRejects undefined / Rejects absent key
    z.coerceundefinedRejects undefined / Rejects absent key
    z.unknown(), z.any()undefinedRejects undefined / Rejects absent key
    .catch(value)optional (at runtime)Returns value (sets fallback flag)
    .default(value)optionalReturns value (does NOT set fallback)
    .prefault(value)optionalReturns value (does NOT set fallback)
    .transform(fn)optional (at runtime)Runs fn (sets fallback flag)
    .preprocess(fn, schema)optional (at runtime)Runs fn (sets fallback flag)
    .optional()optionalAllows absence; may clobber inner fallback values
    // Static type says required, but runtime accepts undefined
    const schema = z.string().catch("fallback");
    
    type Input = z.input<typeof schema>; // { a: string }
    
    // Runtime behavior
    schema.parse(undefined); // returns "fallback"
  8. Simulate nominal typing with Branded types

    main

    TypeScript is structural, meaning types with the same shape are considered identical. To simulate nominal typing (where types are distinct even if they have the same shape), use .brand<T>(). This attaches a unique brand to the inferred type, preventing accidental assignments between structurally identical but logically different types.

    Key behaviors:

    • Branded types are a static-only construct; they do not affect the runtime result of .parse.
    • You must parse data through the schema to obtain the branded type.
    • By default, only the output type is branded. You can customize the branding direction using a second generic parameter (requires Zod 4.2+).

    Branding directions:

    • out (default): Only the output type is branded.
    • in: Only the input type is branded.
    • inout: Both input and output types are branded.
    const Cat = z.object({ name: z.string() }).brand<"Cat">();
    const Dog = z.object({ name: z.string() }).brand<"Dog">();
    
    type Cat = z.infer<typeof Cat>; // { name: string } & z.$brand<"Cat">
    type Dog = z.infer<typeof Dog>; // { name: string } & z.$brand<"Dog">
    
    const pluto = Dog.parse({ name: "pluto" });
    const simba: Cat = pluto; // ❌ not allowed
    
    // Customizing branding direction (Zod 4.2+)
    // output is branded (default)
    z.string().brand<"Cat", "out">(); 
    // input is branded
    z.string().brand<"Cat", "in">(); 
    // both are branded
    z.string().brand<"Cat", "inout">(); 
  9. How `optin` controls input presence

    main

    The optin signal (value: "optional" or undefined) tells parent containers like $ZodObject or $ZodTuple whether they are allowed to omit a slot/key when the input is absent.

    Schema Behavior for optin

    Schema TypeStatic optinRuntime optinBehavior
    $ZodOptional"optional""optional"Hardcoded purpose
    $ZodExactOptional"optional""optional"Same as optional
    $ZodDefault"optional""optional"Hardcoded
    $ZodCatchDefers to inner"optional"Static/Runtime Divergence: Input type shows key as required, but runtime accepts absence
    $ZodTransformInherited"optional"Static/Runtime Divergence: Input type shows key as required, but runtime runs on undefined
    $ZodPipedef.in._zod.optinsameDriven by the leading position
    Everything elseundefinedundefinedRequired by default

    Static/Runtime Divergence Note

    For schemas like z.catch() or z.transform(), the static type (what TypeScript sees) may indicate the field is required, but the runtime parser will accept the field being missing. This allows the schema to recover or transform even when the input is undefined.

  10. Understand ZodError and ZodIssue

    main

    All validation errors thrown by Zod are instances of ZodError. A ZodError is a subclass of Error and contains an issues property, which is an array of ZodIssue objects.

    ZodIssue is a discriminated union representing a specific validation failure. Every issue contains these common fields:

    • code: A z.ZodIssueCode identifying the type of error.
    • path: An array of strings or numbers representing the location of the error (e.g., ['addresses', 0, 'line1']).
    • message: A string describing the error.

    Depending on the code, additional metadata is provided (e.g., expected and received types for invalid_type errors).

    import * as z from "zod";
    
    try {
      // ... validation logic
    } catch (err) {
      if (err instanceof z.ZodError) {
        // Access the array of issues
        console.log(err.issues);
      }
    }
  11. How encoding works with Refinements, Defaults, and Catch

    main

    When calling .encode(), Zod applies specific logic to different schema features:

    • Refinements (.refine(), .min(), etc.): These are executed in both directions. During encoding, Zod performs two passes: first ensuring the input type is valid, then executing the refinement logic. If a refinement fails, it returns a ZodError.
    • Defaults (.default()) and Prefaults: These are only applied in the forward direction (decode). During encode(), undefined is not a valid input and will result in a ZodError rather than triggering the default.
    • Catch (.catch()): This is only applied in the forward direction (decode). During encode(), an invalid input will result in a ZodError rather than the caught value.
  12. What are Codecs and how to use them

    main

    Introduced in zod@4.1, Codecs are special schemas that define a bidirectional transformation between two different types. While most Zod schemas have identical input and output types, codecs allow you to define how to move from an Input type to an Output type (decoding) and back again (encoding).

    This is highly useful for network boundaries, allowing you to share a single schema between a client and server to convert between network-friendly formats (like JSON strings) and rich JavaScript objects (like Date objects).

    const stringToDate = z.codec(
      z.iso.datetime(),  // input schema: ISO date string
      z.date(),          // output schema: Date object
      {
        decode: (isoString) => new Date(isoString), // ISO string → Date
        encode: (date) => date.toISOString(),       // Date → ISO string
      }
    );
    
    // Usage:
    stringToDate.decode("2024-01-15T10:30:00.000Z"); // => Date
    stringToDate.encode(new Date("2024-01-15T10:30:00.000Z")); // => string