Package detail

zod-openapi

samchungy661.5kMIT5.0.1

Convert Zod Schemas to OpenAPI v3.x documentation

typescript, json-schema, swagger, openapi

readme

zod-openapi logo

zod-openapi

A TypeScript library which uses Zod schemas to generate OpenAPI v3.1x documentation.


Installation

Install via npm, yarn, or pnpm:

npm install zod zod-openapi
# or
yarn add zod zod-openapi
# or
pnpm install zod zod-openapi

Usage

.meta()

Use the .meta() method to add OpenAPI metadata to a Zod schema. It accepts an object with the following options:

Option Description
id Registers a schema as a reusable OpenAPI component.
header Adds metadata for response headers.
param Adds metadata for request parameters.
override Allows you to override the rendered OpenAPI schema. This takes either an object or a function.
outputId Allows you to set a different ID for the output schema. This is useful when the input and output schemas differ.
unusedIO Allows you to set the io for an unused schema added to the components section. Defaults to output.

You can also set standard OpenAPI properties directly in the .meta() method, such as:

z.string().meta({
  description: 'A text field',
  example: 'Example value',
});

You can set additional metadata to the rendered schema in a few different ways. Zod's .meta() method allows you to directly set OpenAPI directly to the schema. However, if you wanted to override any field that Zod generates, this may not work as expected. In this case, you can use the override option to customize the rendered OpenAPI schema.

eg.

z.string().datetime().meta({
  description: 'A date field',
  format: 'MY-FORMAT',
  'x-custom-field': 'custom value',
});

Would render the following OpenAPI schema. Note that format is not overridden with our custom format value.

{
  "type": "string",
  "format": "date-time",
  "description": "A date field",
  "x-custom-field": "custom value"
}

In order to override fields which are generated by Zod or this library, you can use the override option in the .meta() method. This allows you to customize the rendered OpenAPI schema after we have both processed it.

override object

This does a naive merge of the override object with the rendered OpenAPI schema. This is useful for simple overrides where you want to add or modify properties without complex logic.

z.string.datetime().meta({
  description: 'A date field',
  override: {
    format: 'MY-FORMAT',
  },
});
override function

The override function allows you to deeply customize the rendered OpenAPI schema. Eg. if you wanted to render a Zod union as a oneOf instead of anyOf for a single schema, you can do so by using the override function. View the documentation for override in the CreateDocumentOptions section for more information.

z.union([z.string(), z.number()]).meta({
  description: 'A union of string and number',
  override: ({ jsonSchema }) => {
    jsonSchema.anyOf = jsonSchema.oneOf;
    delete jsonSchema.oneOf;
  },
});

createDocument

Generates an OpenAPI documentation object.

import * as z from 'zod/v4';
import { createDocument } from 'zod-openapi';

const jobId = z.string().meta({
  description: 'A unique identifier for a job',
  example: '12345',
  id: 'jobId',
});

const title = z.string().meta({
  description: 'Job title',
  example: 'My job',
});

const document = createDocument({
  openapi: '3.1.0',
  info: {
    title: 'My API',
    version: '1.0.0',
  },
  paths: {
    '/jobs/{jobId}': {
      put: {
        requestParams: { path: z.object({ jobId }) },
        requestBody: {
          content: {
            'application/json': { schema: z.object({ title }) },
          },
        },
        responses: {
          '200': {
            description: '200 OK',
            content: {
              'application/json': { schema: z.object({ jobId, title }) },
            },
          },
        },
      },
    },
  },
});
<summary>Creates the following object:</summary> json { "openapi": "3.1.0", "info": { "title": "My API", "version": "1.0.0" }, "paths": { "/jobs/{jobId}": { "put": { "parameters": [ { "in": "path", "name": "jobId", "description": "A unique identifier for a job", "schema": { "$ref": "#/components/schemas/jobId" } } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "title": { "type": "string", "description": "Job title", "example": "My job" } }, "required": ["title"] } } } }, "responses": { "200": { "description": "200 OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "jobId": { "$ref": "#/components/schemas/jobId" }, "title": { "type": "string", "description": "Job title", "example": "My job" } }, "required": ["jobId", "title"] } } } } } } } }, "components": { "schemas": { "jobId": { "type": "string", "description": "A unique identifier for a job", "example": "12345" } } } }

createDocument takes an optional options argument which can be used to modify how the document is created

createDocument(doc, {
  override: ({ jsonSchema, zodSchema, io }) => {
    const def = zodSchema._zod.def;
    if (def.type === 'date' && io === 'output') {
      jsonSchema.type = 'string';
      jsonSchema.format = 'date-time';
    }
  },
  allowEmptySchema: {
    custom: true,
  },
});

CreateDocumentOptions

Option Type Description
override Function Override rendered schema with a function``
outputIdSuffix string Suffix for output schema IDs when the schema is used in both a request and response. Defaults to `'Output'
allowEmptySchema Object Control whether empty schemas are allowed for specific Zod schema types
cycles `'ref' \ 'throw'` How to handle cycles in schemas.
- 'ref' — Break cycles using $defs
- 'throw' — Error on cycles. Defaults to 'ref'
reused `'ref' \ 'inline'` How to handle reused schemas.
- 'ref' — Reused schemas as references
- 'inline' — Inline reused schemas. Defaults to 'inline'
override

The override function allows you to customize the rendered OpenAPI schema. It receives an object with the following properties:

Property Type Description
jsonSchema Object The OpenAPI schema object being generated. You can modify this object to change the rendered schema.
zodSchema ZodType The original Zod schema being converted to OpenAPI. You can use this to access Zod-specific properties.
io `'input' \ 'output'` The context in which the schema is being rendered. 'input' for request bodies/params, 'output' for responses.

This allows you to transform the schema based on your own rules or requirements. For example, if you

  1. Wanted to change how dates are rendered in the output context because a typical JSON serializer will transform them to a string
  2. Change all union outputs to be oneOf instead of anyOf
createDocument(doc, {
  override: ({ jsonSchema, zodSchema, io }) => {
    const def = zodSchema._zod.def;
    if (def.type === 'date' && io === 'output') {
      jsonSchema.type = 'string';
      jsonSchema.format = 'date-time';
      return;
    }
    if (def.type === 'union') {
      jsonSchema.oneOf = jsonSchema.anyOf;
      delete jsonSchema.anyOf;
      return;
    }
  },
});
outputIdSuffix

The outputIdSuffix option allows you to set a suffix for output schema IDs when the schema is used in both a request and response context. This is useful for distinguishing between input and output schemas.

allowEmptySchema

The allowEmptySchema option allows you to control whether or not this library will throw when it encounters an empty schema. By default this library will not throw if it encounters an z.any() or z.unknown() schema.

This allows you to customize how this library handles unrepresentable Zod schemas.

eg. If you wanted to allow z.custom() to be rendered as an empty schema, you can set the allowEmptySchema option as follows:

createDocument(doc, {
  allowEmptySchema: {
    custom: true, // Allow empty schemas for `z.custom()` in all contexts
    set: { output: true }, // Allow empty schemas for `z.set()` only in an output context
  },
});
cycles and reused

These options are exposed directly from the Zod. Read the documentation for more information on how to use these options.

#

createSchema

Creates an OpenAPI Schema Object along with any registered components. OpenAPI 3.1.0 Schema Objects are fully compatible with JSON Schema.

import * as z from 'zod/v4';
import { createSchema } from 'zod-openapi';

const jobId = z.string().meta({
  description: 'A unique identifier for a job',
  example: '12345',
  id: 'jobId',
});

const title = z.string().meta({
  description: 'Job title',
  example: 'My job',
});

const job = z.object({
  jobId,
  title,
});

const { schema, components } = createSchema(job);
<summary>Creates the following object:</summary> json { "schema": { "type": "object", "properties": { "jobId": { "$ref": "#/components/schemas/jobId" }, "title": { "type": "string", "description": "Job title", "example": "My job" } }, "required": ["jobId", "title"] }, "components": { "jobId": { "type": "string", "description": "A unique identifier for a job", "example": "12345" } } }

CreateSchemaOptions

createSchema takes an optional CreateSchemaOptions parameter which includes all options from CreateDocumentOptions plus the following:

const { schema, components } = createSchema(job, {
  // Input/Output context - controls how schemas are generated
  io: 'input', // 'input' for request bodies/params, 'output' for responses
  // Component handling
  schemaComponents: { jobId: z.string() }, // Pre-defined components to use
  schemaComponentRefPath: '#/definitions/', // Custom path prefix for component references
  opts: {}, // Create Document Options,
});

Request Parameters

Query, Path, Header & Cookie parameters can be created using the requestParams key under the method key as follows:

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() }),
        },
      },
    },
  },
});

If you would like to declare parameters in a more traditional way you may also declare them using the parameters key. The definitions will then all be combined.

createDocument({
  paths: {
    '/jobs/{a}': {
      put: {
        parameters: [
          z.string().meta({
            param: {
              name: 'job-header',
              in: 'header',
            },
          }),
        ],
      },
    },
  },
});

Request Body

Where you would normally declare the media type, set the schema as your Zod Schema as follows.

createDocument({
  paths: {
    '/jobs': {
      get: {
        requestBody: {
          content: {
            'application/json': { schema: z.object({ a: z.string() }) },
          },
        },
      },
    },
  },
});

If you wish to use OpenAPI syntax for your schemas, simply add an OpenAPI schema to the schema field instead.

Responses

Similarly to the Request Body, simply set the schema as your Zod Schema as follows. You can set the response headers using the headers key.

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(),
            }),
          },
        },
      },
    },
  },
});

Callbacks

createDocument({
  paths: {
    '/jobs': {
      get: {
        callbacks: {
          onData: {
            '{$request.query.callbackUrl}/data': {
              post: {
                requestBody: {
                  content: {
                    'application/json': { schema: z.object({ a: z.string() }) },
                  },
                },
                responses: {
                  200: {
                    description: '200 OK',
                    content: {
                      'application/json': {
                        schema: z.object({ a: z.string() }),
                      },
                    },
                  },
                },
              },
            },
          },
        },
      },
    },
  },
});

Creating Components

OpenAPI allows you to define reusable components and this library allows you to replicate that in two separate ways.

  1. Auto registering schema
  2. Manually registering schema

Schema

If we take the example in createDocument and instead create title as follows

Auto Registering Schema
const title = z.string().meta({
  description: 'Job title',
  example: 'My job',
  id: 'jobTitle', // <- new field
});

Wherever title is used in schemas across the document, it will instead be created as a reference.

{ "$ref": "#/components/schemas/jobTitle" }

title will then be outputted as a schema within the components section of the documentation.

{
  "components": {
    "schemas": {
      "jobTitle": {
        "type": "string",
        "description": "Job title",
        "example": "My job"
      }
    }
  }
}

This is a great way to create less repetitive Open API documentation. There are some Open API features like discriminator mapping which require all schemas in the union to contain a ref.

Manually Registering Schema

Another way to register schema instead of adding a ref is to add it to the components directly. This will still work in the same way as ref. So whenever we run into that Zod type we will replace it with a reference.

eg.

const title = z.string().meta({
  description: 'Job title',
  example: 'My job',
});
createDocument({
  components: {
    schemas: {
      jobTitle: title, // this will register this Zod Schema as jobTitle unless `id` in `.meta()` is specified on the type
    },
  },
});

Parameters

Query, Path, Header & Cookie parameters can be similarly registered:

// Easy auto registration
const jobId = z.string().meta({
  description: 'Job ID',
  example: '1234',
  param: { id: 'jobRef' },
});

createDocument({
  paths: {
    '/jobs/{jobId}': {
      put: {
        requestParams: {
          header: z.object({
            jobId,
          }),
        },
      },
    },
  },
});

// or more verbose auto registration
const jobId = z.string().meta({
  description: 'Job ID',
  example: '1234',
  param: { in: 'header', name: 'jobId', id: 'jobRef' },
});

createDocument({
  paths: {
    '/jobs/{jobId}': {
      put: {
        parameters: [jobId],
      },
    },
  },
});

// or manual registration
const otherJobId = z.string().meta({
  description: 'Job ID',
  example: '1234',
  param: { in: 'header', name: 'jobId' },
});

createDocument({
  components: {
    parameters: {
      jobRef: jobId,
    },
  },
});

Response Headers

Response headers can be similarly registered:

const header = z.string().meta({
  description: 'Job ID',
  example: '1234',
  header: { id: 'some-header' },
});

// or

const jobIdHeader = z.string().meta({
  description: 'Job ID',
  example: '1234',
});

createDocument({
  components: {
    headers: {
      someHeaderRef: jobIdHeader,
    },
  },
});

Responses

Entire Responses can also be registered

const response: ZodOpenApiResponseObject = {
  description: '200 OK',
  content: {
    'application/json': {
      schema: z.object({ a: z.string() }),
    },
  },
  id: 'some-response',
};

//or

const response: ZodOpenApiResponseObject = {
  description: '200 OK',
  content: {
    'application/json': {
      schema: z.object({ a: z.string() }),
    },
  },
};

createDocument({
  components: {
    responses: {
      'some-response': response,
    },
  },
});

Callbacks

Callbacks can also be registered

const callback: ZodOpenApiCallbackObject = {
  id: 'some-callback'
  post: {
    responses: {
      200: {
        description: '200 OK',
        content: {
          'application/json': {
            schema: z.object({ a: z.string() }),
          },
        },
      },
    },
  },
};

//or

const callback: ZodOpenApiCallbackObject = {
  post: {
    responses: {
      200: {
        description: '200 OK',
        content: {
          'application/json': {
            schema: z.object({ a: z.string() }),
          },
        },
      },
    },
  },
};

createDocument({
  components: {
    callbacks: {
      'some-callback': callback,
    },
  },
});

Path Items

Path Items can also be registered

const pathItem: ZodOpenApiPathItemObject = {
  id: 'some-path-item',
  get: {
    responses: {
      200: {
        description: '200 OK',
        content: {
          'application/json': {
            schema: z.object({ a: z.string() }),
          },
        },
      },
    },
  },
};

// or

createDocument({
  components: {
    pathItems: {
      'some-path-item': pathItem,
    },
  },
});

Security Schemes

Security Schemes can be registered for authentication methods:

createDocument({
  components: {
    securitySchemes: {
      bearerAuth: {
        type: 'http',
        scheme: 'bearer',
        bearerFormat: 'JWT',
        description: 'JWT Authentication',
      },
    },
  },
});

Links

Links can be registered to describe relationships between operations:

const link: ZodOpenApiLinkObject = {
  id: 'getUserById',
  operationId: 'getUser',
  parameters: {
    userId: '$request.path.id',
  },
  description: 'Link to get user by id',
};

// or

createDocument({
  components: {
    links: {
      getUserById: {
        operationId: 'getUser',
        parameters: {
          userId: '$request.path.id',
        },
        description: 'Link to get user by id',
      },
    },
  },
});

Examples

Examples can be registered to provide sample values for schemas:

const example: ZodOpenApiExampleObject = {
  id: 'userExample',
  summary: 'A sample user',
  value: {
    id: '123',
    name: 'Jane Doe',
    email: 'jane@example.com',
  },
};

// or

createDocument({
  components: {
    examples: {
      userExample: {
        summary: 'A sample user',
        value: {
          id: '123',
          name: 'Jane Doe',
          email: 'jane@example.com',
        },
      },
    },
  },
});

Zod Types

Zod types are composed of two different parts: the input and the output. This library decides which type to create based on if it is used in a request or response context.

Input:

  • Request Parameters (query, path, header, cookie)
  • Request Body

Output:

  • Response Body
  • Response Headers

In general, you want to avoid using a registered input schema in an output context and vice versa. This is because the rendered input and output schemas of a simple Zod schema will differ, even with a simple Zod schema like z.object().

const schema = z.object({
  name: z.string(),
});

Input schemas (request bodies, parameters):

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    }
  },
  "required": ["name"]
}

Output schemas (responses):

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    }
  },
  "required": ["name"],
  "additionalProperties": false
}

When the same schema is referenced in both input and output contexts, the library generates two separate component schemas. This happens automatically when a schema with an ID is used in both contexts.

You can customize the output schema name by providing an outputId:

const schema = z
  .object({
    name: z.string(),
  })
  .meta({
    id: 'MyObject',
    outputId: 'MyObjectResponse', // Customize the output schema name
  });

You can also set a global suffix for output schemas or use z.looseObject() and z.strictObject() to have explicit control over the schema behavior.

⚠️ Note: If your registered schema contains dynamically created lazy components, they won't be reused between input and output schemas.

Supported OpenAPI Versions

Currently the following versions of OpenAPI are supported

  • 3.1.0 (minimum version)
  • 3.1.1

Setting the openapi field will change how the some of the components are rendered.

createDocument({
  openapi: '3.1.0',
});

As an example z.string().nullable() will be rendered differently

3.0.0

{
  "type": "string",
  "nullable": true
}

3.1.0

{
  "type": ["string", "null"]
}

Examples

See the library in use in the examples folder.

Ecosystem

  • fastify-zod-openapi - Fastify plugin for zod-openapi. This includes type provider, Zod schema validation, Zod schema serialization and Swagger UI support.

  • eslint-plugin-zod-openapi - Eslint rules for zod-openapi. This includes features which can autogenerate Typescript comments for your Zod types based on your description, example and deprecated fields.

Version Information

For information about changes and migration from v4 to v5, see the v5 migration guide.

Development

Prerequisites

  • Node.js LTS
  • pnpm
pnpm
pnpm build

Test

pnpm test

Lint

# Fix issues
pnpm format

# Check for issues
pnpm lint

changelog

zod-openapi

5.0.1

Patch Changes

  • d71fe72: Preserve non Zod schemas