zod-openapi

repository·master·Indexed 20 days ago

https://github.com/samchungy/zod-openapi

A TypeScript library that converts Zod schemas into OpenAPI v3.x documentation, allowing Zod schemas to serve as the single source of truth for data validation and API specifications. It features native .meta() support for OpenAPI metadata, automatic generation of input and output schema contexts, and a registry-based lazy resolution model introduced in version 5.

Tokens
10.6K
Snippets
36
Records
42
Agent score
70%

What's inside zod-openapi

  1. Register OpenAPI Components (Schemas, Parameters, etc.)

    master

    You can register reusable components in two ways:

    1. Auto-registration: Add an id to the .meta() call of a Zod schema or parameter. The library will automatically create a $ref in the document and add the schema to the components section.
    2. Manual registration: Explicitly define the component in the components property of the createDocument configuration object.

    Supported component types include:

    • schemas
    • parameters
    • headers
    • responses
    • callbacks
    • pathItems
    • securitySchemes
    • links
    • examples
    // 1. Auto-registration via .meta({ id: '...' })
    const jobId = z.string().meta({
      id: 'jobRef',
      param: { in: 'header', name: 'jobId' },
    });
    
    createDocument({
      paths: {
        '/jobs/{jobId}': {
          put: {
            requestParams: { header: z.object({ jobId }) },
          },
        },
      },
    });
    
    // 2. Manual registration
    createDocument({
      components: {
        parameters: {
          jobRef: z.string().meta({ param: { in: 'header', name: 'jobId' } }),
        },
      },
    });
  2. Understanding Input vs Output Schema Contexts

    master

    Zod types are composed of an input and an output. This library distinguishes between these contexts to generate appropriate OpenAPI schemas:

    • Input Context: Used for Request Parameters (query, path, header, cookie) and Request Bodies.
    • Output Context: Used for Response Bodies and Response Headers.

    Why this matters: A single Zod schema might render differently depending on the context. For example, a z.object() in an input context might not include additionalProperties: false, whereas in an output context, it might.

    If a schema is used in both contexts, the library automatically generates two separate component schemas to avoid conflicts. You can customize the output schema name using .meta({ outputId: '...' }) or set a global suffix via createDocument's outputIdSuffix option.

    // Customizing the output schema name to distinguish from input
    const schema = z
      .object({
        name: z.string(),
      })
      .meta({
        id: 'MyObject',
        outputId: 'MyObjectResponse',
      });
  3. Use the native .meta() method for schema metadata

    master

    In v5, zod-openapi uses native Zod metadata via the .meta() method, replacing the need for runtime extensions and monkey-patching. You no longer need to call extendZodWithOpenApi(z) or import zod-openapi/extend.

    Replace schema definitions:

    - z.string().openapi({ ... })
    + z.string().meta({ ... })

    Note on dependencies: Because metadata is handled natively, you can declare zod-openapi as a devDependency if you only use it for build-time schema generation.

    // Old v4 way
    z.string().openapi({ description: 'a string' })
    
    // New v5 way
    z.string().meta({ description: 'a string' })
  4. Understand Input and Output schema generation (Automatic Dual Schemas)

    master

    zod-openapi v5 uses Zod's native toJSONSchema() method. This results in different behaviors for input and output schemas to better align with how Zod objects work:

    • Input schemas: Allow additional properties by default (no additionalProperties specified).
    • Output schemas: Strip extra properties (additionalProperties: false).

    If an object schema with an id is used in both request (input) and response (output) contexts, the library automatically generates two separate component schemas (e.g., Person and PersonOutput).

    Customization

    • Custom output name: Use .meta({ id: 'Person', outputId: 'CustomOutputName' }).
    • Global suffix: Set outputIdSuffix in CreateDocumentOptions to change the default "Output" suffix.
    • Manual control: Use z.looseObject() (allows additional properties) or z.strictObject() (forbids them) to explicitly control behavior instead of the default z.object().

    Note: If a schema contains dynamically created lazy components, they will not be reused between input and output schemas.

    const schema = z
      .object({
        name: z.string(),
        age: z.number(),
      })
      .meta({
        id: 'Person',
      });
  5. How schema generation works in zod-openapi v5

    master

    In v5, schema generation has moved from an immediate creation model to a registry-based lazy resolution model. Instead of functions returning fully populated schemas immediately, you now use a registry to manage schemas.

    1. Create a Registry: Initialize a registry using createRegistry().
    2. Add Schemas to Registry: Use registry.addSchema() to register your Zod schemas. This returns an empty reference object initially.
    3. Generate Components: Call createComponents(registry, docOpts) to populate the registry, resolve all references, and generate the final schemas and components.

    This approach enables better deduplication and automated lazy schema resolution.

    // 1. Create a registry
    const registry = createRegistry();
    
    // 2. Add your schema to the registry (returns a reference)
    const schema = registry.addSchema({
      schema,
      path,
      source: {
        type: 'mediaType',
      }
    });
    
    console.log(schema); // {} (empty reference)
    
    // 3. Generate all schemas and components at once
    const components = createComponents(registry, docOpts);
    
    console.log(schema); // { type: 'string' } (now populated)
  6. Update Zod and Node.js requirements for v5

    master

    zod-openapi v5 has updated its runtime requirements:

    1. Zod v4 (3.25.74) required: You must import Zod using the v4 syntax.
      - import { z } from 'zod';
      + import * as z from 'zod/v4';
    2. Node.js v20+ required: Support for Node v18 has been dropped.
  7. Define Request Parameters (Path, Query, Header, Cookie)

    master

    You can define request parameters in two ways:

    1. Using requestParams: Group parameters by their location (path, query, cookie, or header) using a Zod object.
    2. Using parameters: Use the traditional OpenAPI parameters array, where you can use .meta({ param: { ... } }) on Zod schemas to specify the location and name.

    Note: When using .meta({ param: { ... } }), the id in .meta() can be used to register the parameter as a reusable component.

    // Method 1: requestParams
    createDocument({
      paths: {
        '/jobs/{a}': {
          put: {
            requestParams: {
              path: z.object({ a: z.string() }),
              query: z.object({ b: z.string() }),
              cookie: z.object({ cookie: z.string() }),
              header: z.object({ 'custom-header': z.string() }),
            },
          },
        },
      },
    });
    
    // Method 2: parameters array with .meta()
    createDocument({
      paths: {
        '/jobs/{a}': {
          put: {
            parameters: [
              z.string().meta({
                param: {
                  name: 'job-header',
                  in: 'header',
                },
              }),
            ],
          },
        },
      },
    });
  8. Define Request Bodies and Responses

    master

    Request Body

    Set the schema field within the content object of the desired media type (e.g., 'application/json').

    Responses

    Set the schema field within the content object of the response status code. You can also define response headers using the headers key with a Zod schema.

    // Request Body
    createDocument({
      paths: {
        '/jobs': {
          get: {
            requestBody: {
              content: {
                'application/json': { schema: z.object({ a: z.string() }) },
              },
            },
          },
        },
      },
    });
    
    // Responses with Headers
    createDocument({
      paths: {
        '/jobs': {
          get: {
            responses: {
              200: {
                description: '200 OK',
                content: {
                  'application/json': { schema: z.object({ a: z.string() }) },
                },
                headers: z.object({
                  'header-key': z.string(),
                }),
              },
            },
          },
        },
      },
    });
  9. Migrate from zod-openapi v4 to v5

    master

    To migrate your codebase from v4 to v5, you can use the provided codemod. This is recommended to handle the significant changes in schema definitions and metadata usage.

    Run the codemod using either pnpmx or npx:

    pnpmx codemod-zod-openapi-v5 'src/**/*.ts'
    # or
    npx codemod-zod-openapi-v5 'src/**/*.ts'
    npx codemod-zod-openapi-v5 'src/**/*.ts'
  10. Migrate from zod-openapi v4 to v5

    master

    If you are upgrading from v4 to v5, you must adopt the new registry-based workflow and replace several deprecated functions. The primary change is that schemas are no longer generated immediately upon function call but are populated when createComponents() is invoked on a registry.

    ### Replaced Functions Reference
    
    | ❌ Removed (v4)         | ✅ Replacement (v5)       |
    | ----------------------- | ------------------------- |
    | `createParamOrRef`      | `registry.addParameter()` |
    | `createMediaTypeSchema` | `registry.addSchema()`     |
    | `getZodObject`          | `unwrapZodObject`         |
    | `getDefaultComponents`  | `createRegistry()`         |