zod-openapi: generating OpenAPI documents from the schemas you already have
Use Zod Schemas to create OpenAPI v3.x documentation
At a glance
- What is it?
- zod-openapi turns Zod schemas into an OpenAPI v3.x document, using Zod's own .meta() method for annotations rather than a parallel schema definition. The interesting design decisions are the override mechanism for fields Zod generates, and the split release cadence between the runtime package and the OpenAPI typings.
- Who is it for?
- Adopt zod-openapi if your TypeScript service already validates with Zod and you want the API documentation to come from the same source of truth rather than a hand-written OpenAPI file that drifts. Do not adopt it if you need to describe responses you do not model with Zod, since the document is derived from your schemas and cannot describe what your schemas do not describe.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 54 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One schema, two consumers: validation and documentation
The pitch is one line in the README: a TypeScript library which uses Zod schemas to generate OpenAPI v3.x documentation.
The problem it addresses is familiar to anybody who has maintained both a Zod validation layer and an OpenAPI document. The two drift. Somebody adds a field to a request body, the Zod schema changes, and the OpenAPI file does not, and the drift is only discovered when a client generated from the specification fails against a running server.
zod-openapi removes the second source of truth by making the documentation a projection of the schemas. The Zod schema remains the thing you use at runtime for validation, and the OpenAPI document is generated from it, so a change to the schema is a change to the documentation by construction.
The library is MIT licensed with the terms in the LICENSE file, published to npm as `zod-openapi`, and the repository is not archived. The last push was on 2026-08-31 and the current release is 6.0.2, with 6.0.1 on 2026-08-13 and 6.0.0 on 2026-06-14. The package declares `sideEffects: false`, is ESM-first with `type: module`, and ships dual entry points with `import` and `require` conditions alongside a `types` field.
It is worth noting the size of the thing. The dependency list is short and the manifest lists a workspace package, `@zod-openapi/openapi3-ts`, which supplies the OpenAPI typing layer.
Metadata goes through Zod's own .meta(), not a wrapper
The integration point is the part that decides whether this library feels native or bolted on, and the README is explicit about it. It uses Zod's built-in metadata functionality, with no monkey patching or additional setup required. You simply call `.meta()` on any Zod schema.
z.string().meta({
description: 'A text field',
example: 'Example value',
});Beyond the standard OpenAPI properties, `.meta()` accepts a set of library-specific options. `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, taking either an object or a function. `outputId` sets a different ID for the output schema, which is useful when the input and output schemas differ. And `unusedIO` sets the `io` for an unused schema added to the components section, defaulting to `output`.
The `id` option is the mechanism behind the component references in the generated document. In the worked example from the README, a schema given `id: 'jobId'` becomes a `#/components/schemas/jobId` reference in the path's parameters, which is what makes the document reusable rather than repeating the same shape inline.
There is one setup detail that catches people out. For full TypeScript support of the OpenAPI properties inside `.meta()`, you need to import `zod-openapi` somewhere in your project, or add a `/// <reference types="zod-openapi" />` anywhere in it. Without that, the extra options are not typed, and you would be relying on TypeScript to complain about nothing.
Zod owns some fields, and override is how you win them back
The limitation that this library most explicitly addresses is that Zod generates some fields in the output, and setting a property in `.meta()` does not always change them.
The README gives the example. A schema declared as `z.string().datetime()` with a `format: 'MY-FORMAT'` in its metadata renders as:
{
"type": "string",
"format": "date-time",
"description": "A date field",
"x-custom-field": "custom value"
}The note is direct: format is not overridden with the custom format value. The `description` and the custom `x-custom-field` came through, but the `format` stayed as `date-time`, because Zod decided that one.
There are two ways to fight that, and the README covers both.
The `override` object does a naive merge of the override object with the rendered OpenAPI schema, and it is described as 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',
},
});The `override` function is the deeper option, and it receives the rendered schema so you can rewrite it as you like. The example the README gives is turning a Zod union into a `oneOf` rather than the `anyOf` it would otherwise render as:
z.union([z.string(), z.number()]).meta({
description: 'A union of string and number',
override: ({ jsonSchema }) => {
jsonSchema.anyOf = jsonSchema.oneOf;
delete jsonSchema.oneOf;
},
});The two forms are a genuine API decision. The object form is convenient and shallow, the function form is where the real power is, and the choice of providing both is one of the more thoughtful things about the library.
createDocument takes paths as Zod schemas, not as OpenAPI fragments
The document builder is the other half of the library, and its shape is where the value shows.
The import is version-sensitive, which the README flags in a comment on the first line of the example. It is `import * as z from 'zod/v4'`, or `import * as z from 'zod'` if using Zod 4.0.0 or later, and then `import { createDocument } from 'zod-openapi'`.
The document argument is OpenAPI-shaped, but the schemas inside it are Zod schemas. A path entry has a `requestParams` holding a `path` object built from `z.object({ jobId })`, a `requestBody` whose `content` maps a media type to a Zod schema, and `responses` whose `200` entry has a `description` and a `content` mapping to a Zod schema. The `info` block and the `openapi` version string are ordinary OpenAPI values.
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 }) },
},
},
},
},
},
},
});The `requestParams` key with a nested `path` is worth pausing on, because it is not standard OpenAPI. Standard OpenAPI has `parameters` as an array on the operation, with `in: path` marking the location. Here the parameters are grouped by location in an object, which is more convenient to write in TypeScript and which the library then converts into the array form the specification requires.
The document and the types ship as separate versioned packages
The manifest shows a monorepo structure and an unusual release arrangement that a user of the library will feel.
The root `package.json` publishes `zod-openapi` at version 6.0.2, and the export map has two conditions. The `.` entry exposes the source under `zod-openapi/source`, the built ESM under `import` and the CommonJS build under `require`. The `./api` entry does the same for a separate `api.ts` module, which is the OpenAPI typing layer, and the `files` array ships just `lib` and `api`.
The dev dependencies include `@zod-openapi/openapi3-ts` at `workspace:*`, which tells you the OpenAPI typings are a workspace package in their own right, versioned independently of the runtime. The release history bears this out. The listed releases are `[email protected]` on 2026-08-31, `[email protected]` on 2026-08-13 and `[email protected]` on 2026-06-14, all under the `zod-openapi@` scope, so the runtime package is versioned on its own line while the typings package carries a separate version.
The build tooling is `skuba`, which appears throughout the scripts. The build is `skuba build-package`, the test is `skuba test` with a coverage variant, and the format and lint tasks are `skuba format` and `skuba lint`. There is a `copy:types` step, and the prepare script runs that plus the build.
The release process uses changesets, with a `release` script that builds and then runs `changeset publish`, and there is a `.changeset/` directory at the top level. The repository also has a `CHANGELOG.md`, a `tsdown.config.mts` for the bundler, a `vitest.config.ts` for tests, and an `eslint.config.js` with an `eslint-plugin-zod-openapi` in the dev dependencies, which is a lint rule set written for this library's own schemas.
The docs pipeline is documented in one script. `create:docs` runs the example schema script and then builds static documentation with the Redocly CLI into `examples/simple/redoc-static.html`, so the example directory doubles as the documentation source.
Compared with hand-written OpenAPI and with a runtime-first generator
The alternative approaches differ in what they ask you to keep in sync.
A hand-written OpenAPI file, or one generated from JSDoc comments and decorators, treats the specification as the primary artifact and the code as the implementation. That has a real advantage in one direction: you can describe responses that your code does not model, including error shapes from upstream services, pagination envelopes and vendor-specific fields. It also has a well-known failure mode, which is that the document and the implementation are separate things and one of them is wrong.
zod-openapi inverts the relationship. The schemas are primary and the document is derived. That means the document is always consistent with the validation behavior, which is the failure that actually bites production systems. The cost is that you can only document what you have modeled, and the cost of modeling something you did not model before is real.
The third approach is a runtime-first generator, in the family of the tools that produce a specification by capturing the requests your server actually handles, or by wrapping your validation layer so the specification is derived from the same call. The difference from zod-openapi is where the derivation happens. A runtime capture tool produces a document that describes what the code does, including its bugs, and it needs the application running. zod-openapi produces a document from a static description of your schemas, at build time, with no server involved.
That build-time property matters for a specific case. A team with a serverless or edge deployment, where there is no long-running process to capture traffic from, or a team that generates the specification in CI and commits the result for client teams to consume, gets a deterministic artifact from zod-openapi.
The remaining consideration is the version of the specification. The example uses `openapi: '3.1.0'`, and the library describes itself as generating OpenAPI v3.x documentation. The 3.1 line is what aligns with JSON Schema, which is what a Zod schema is, and that alignment is the reason the generated output is a faithful projection rather than a translation.
Editorial conclusion
Adopt zod-openapi if your TypeScript service already validates with Zod and you want the API documentation to come from the same source of truth rather than a hand-written OpenAPI file that drifts. Do not adopt it if you need to describe responses you do not model with Zod, since the document is derived from your schemas and cannot describe what your schemas do not describe. Verify first that your Zod version supports the native .meta() method, that the OpenAPI version you target matches the document you generate, and that the override function form is used for anything Zod renders in a way you do not agree with.
Frequently asked questions
How do I install zod-openapi?
The README gives three options: `npm install zod zod-openapi`, `yarn add zod zod-openapi` or `pnpm install zod zod-openapi`. Zod itself is a required peer, since the library reads your Zod schemas to produce the document.
How do I add OpenAPI metadata to a Zod schema?
Call Zod's native `.meta()` method on the schema with an object of OpenAPI properties such as `description` and `example`, plus the library's own options like `id`, `header`, `param`, `override`, `outputId` and `unusedIO`. The README says no monkey patching or additional setup is required.
Why is my format field not overridden in zod-openapi?
Fields that Zod generates are not changed by setting the same property in `.meta()`, as the README shows with a `z.string().datetime()` schema whose `format` stays `date-time`. To change those, use the `override` option, either as a plain object for a naive merge or as a function that receives the rendered schema and can rewrite it.
What does createDocument produce in zod-openapi?
It generates an OpenAPI documentation object from an argument that mixes OpenAPI structure with Zod schemas, so paths carry `requestParams`, `requestBody` and `responses` whose content maps media types to Zod schemas. The README's example produces an OpenAPI 3.1.0 document with reusable component references for schemas registered with an `id`.
Do I need to do anything special to get TypeScript support for the meta options?
Yes. The README says to import `zod-openapi` in your project or add a `/// <reference types="zod-openapi" />` anywhere in it, which provides the compile-time definitions for properties like `id`, `param` and `header`.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/samchungy-zod-openapi)Was this useful?