zod-to-openapi unifies Zod validation and OpenAPI docs
A library that generates OpenAPI (Swagger) docs from Zod schemas
At a glance
- What is it?
- zod-to-openapi generates an OpenAPI document from the same Zod schemas you validate with, so a TypeScript API can stop maintaining a separate Swagger definition. Version 9 targets Zod v4 as a peer dependency, and the README keeps a pinned 7.3.4 branch alive for teams still on Zod v3.
- Who is it for?
- Adopt zod-to-openapi when your TypeScript API already validates with Zod and you are tired of keeping a parallel OpenAPI definition in sync, and use plain `.meta()` if all you need is schema names and descriptions. Do not start a new Zod v3 project on this library: the README pins 7.3.4 for v3 and states plainly that version is not intended to be supported going forward.
- 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 36 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 November 6, 2026, and from our analysis. They are not legal advice.
Star history
- asteasolutions/zod-to-openapi1,635 ★ · +4
| Repository | 2026-10-16 | 2026-11-06 |
|---|---|---|
| asteasolutions/zod-to-openapi | 1.6k | 1.6k |
4 Stars in the last 21 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
The duplication problem, stated plainly
The library exists because of a specific duplication. The README says the authors at Astea Solutions use Zod for validation in their APIs and were tired of maintaining a separate OpenAPI definition that has to be kept in sync. Every field in the API therefore existed twice: once as a runtime validator and once as documentation.
The approach is to register paths and schemas against a registry object, and Zod schemas carry the OpenAPI metadata inline. A schema registers itself by name:
const UserSchema = z
.object({
id: z.string().openapi({ example: '1212121' }),
name: z.string().openapi({ example: 'John Doe' }),
age: z.number().openapi({ example: 42 }),
})
.openapi('User');The same object then serves both jobs. The README is explicit that you can still use `UserSchema` and the `request.params` object to validate the input of your API after the document has been generated. That is the whole argument, and it holds only if your validators survive into the production bundle.
For teams that already generate OpenAPI from type definitions, the difference is where the definitions come from. Here they are runtime objects, which is also where the sharp edges live.
Three overloads of one openapi method
To keep definitions readable, the library adds an `openapi` method to Zod objects. It has three overloads, and the choice between them is the main design decision you make per schema.
The first takes only metadata: `.openapi({ [key]: value })`, so `z.number().openapi({ example: 3 })` adds `example: 3` to the generated schema. The second takes only a name: `.openapi("<schema-name>")`, which registers the schema into `components/schemas` under that name. The third combines both. Registering is what produces a `$ref` in the output rather than an inline expansion, so a schema referenced by several routes should be registered once.
Enabling the method requires one call, placed in a common entrypoint file such as `index.ts` or `app.ts`:
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';
extendZodWithOpenApi(z);
// We can now use `.openapi()` to specify OpenAPI metadata
z.string().openapi({ description: 'Some string' });The README flags two practical frictions. If you tree-shake with Webpack you must mark that file as having side effects, because the extension is a mutation of the Zod namespace rather than an import you reference. And ordering matters, since `require` is synchronous while `import` is async, so the extension call has to land before any schema that uses `.openapi` is constructed.
Zod v4's .meta covers most cases without extendZodWithOpenApi
From v8 of this library, paired with Zod v4, you can use Zod's own `.meta` instead. The README states that the two forms produce exactly the same results:
const schema = z
.string()
.openapi('Schema', { description: 'Name of the user', example: 'Test' });
const schema2 = z
.string()
.meta({ id: 'Schema2', description: 'Name of the user', example: 'Test' });The note under that example is the practically useful part. Unless you are in one of the more complicated scenarios, you could generate a schema without calling `extendZodWithOpenApi` at all and rely purely on `.meta` for metadata and for schema names via the `id` property. That removes the global mutation, and with it the Webpack side-effects flag and the import-ordering problem.
Two scenarios are listed as genuinely requiring the older mechanism. The first is extending a registered schema, where the extended schema references the original through `anyOf`:
const schema = z.object({ name: z.string() }).openapi('Schema');
const schema2 = schema.extend({ age: z.number() }).openapi('Schema2');The second is parameter-level metadata, where a description has to land on the OpenAPI `parameters` entry rather than on the schema. You express it by nesting a `param` object inside the metadata:
request: {
query: z.object({
name: z.string().openapi({
description: 'Schema level description',
param: { description: 'Param level description' },
}),
}),
},The generated output splits them, with the schema-level description inside `schema` and the `param` description promoted to the parameter's own `description`, alongside `required`, `name` and `in: query`.
The Zod v3 line is frozen at 7.3.4
The first thing in the README is a callout, not a title. It says that for Zod v3 support you should use version 7.3.4, and that the maintainers do not intend to actively support that version going forward. The install line it gives is:
npm install @asteasolutions/[email protected]That is a policy statement about a fork in the road, and it is reinforced by `package.json`, which declares `zod: ^4.0.0` as a peer dependency for the current release. A team on Zod v3 is therefore choosing between staying on an explicitly unmaintained pin or scheduling the Zod v4 upgrade.
The versioning history supports the same reading. Recent releases are v9.1.0 from 2026-07-19, v9.0.0 from 2026-07-14, and v8.5.0 from 2026-03-20.
Packaging, build and what the test suite covers
The distribution is a plain dual-format package. `package.json` points `main` at `dist/index.cjs`, `module` at `dist/index.mjs` and `types` at `dist/index.d.ts`, and the published `files` list is only `dist`, `package.json`, `LICENSE` and `README.md`. There is one runtime dependency, `openapi3-ts` at `^4.6.0`, which is the type model the generated document is assembled against.
The build is Rollup through `rollup -c`, wired to `prepare` and `prepublishOnly` so a publish always rebuilds. Formatting and linting are both Prettier, with `lint` running `prettier --check.`.
Tests come in two flavours and both run in `test`. `test:jest` runs Jest, and `test:types` runs `tsd` against a type definition fixture:
"test:types": "npx tsd --files spec/type-definitions/zod-extensions.test-d.ts --typings dist/index.d.ts"The `spec/` directory at the repository root is where those type-level expectations live, which is a hint that a good part of this library's contract is expressed in types rather than runtime behaviour. For a package whose main job is transforming object literals into a document, that is the right place to put the guarantees.
When this is the wrong tool
The main wrong case is a Zod v3 codebase with no plan to move. The README is unambiguous that 7.3.4 is not intended to be actively supported, so adopting it there means owning a frozen dependency from day one.
The second wrong case is a project whose schemas are not available at runtime. This library inspects live Zod objects, so if your build strips validators, tree-shakes them away, or your schemas only exist as TypeScript types, there is nothing for the generator to read. Adding this dependency does not change that; it makes the requirement explicit.
The third is a service with no Zod at all. The peer dependency range `^4.0.0` means you would be adopting a validation library in order to generate documentation, which inverts the usual order and adds a runtime dependency to a codebase that may have chosen a different validator deliberately. In that situation, a generator driven by your existing type definitions is the honest choice, and the trade-off is exactly what this library exists to avoid: two sources of truth.
One documentation gap is worth naming. The README's table of contents lists sections for the registry, the generator, defining routes and webhooks, custom components, a full example, adding it to your build and generation options, but the text available here is truncated partway through the usage walkthrough. The repository also contains `example/index.ts` and its companion `example/openapi-docs.yml`.
Editorial conclusion
Adopt zod-to-openapi when your TypeScript API already validates with Zod and you are tired of keeping a parallel OpenAPI definition in sync, and use plain `.meta()` if all you need is schema names and descriptions. Do not start a new Zod v3 project on this library: the README pins 7.3.4 for v3 and states plainly that version is not intended to be supported going forward. Before upgrading a v3 codebase, check that your Zod version satisfies the `zod: ^4.0.0` peer range, decide whether you need `extendZodWithOpenApi` at all or can rely on `.meta` alone, and confirm your build still makes the Zod schemas available to the generator, since the whole tool depends on Zod schemas surviving to runtime.
Frequently asked questions
How do I install zod-to-openapi?
The README gives `npm install @asteasolutions/zod-to-openapi` or `yarn add @asteasolutions/zod-to-openapi`. For Zod v3 it pins a different line: `npm install @asteasolutions/[email protected]`.
Do I need extendZodWithOpenApi when using Zod v4?
The README says that unless you are in one of the more complicated scenarios you could generate a schema without calling extendZodWithOpenApi and rely on `.meta` for metadata and the `id` property for schema names. The listed exceptions are extending registered schemas that should reference the original via anyOf, and parameter-level metadata.
Which Zod version does the current zod-to-openapi release require?
The package.json in the repository declares zod ^4.0.0 as a peer dependency for version 9.1.0, with zod ^4.0.5 among the development dependencies. The README routes Zod v3 users to the older 7.3.4 release and says that version is not intended to be actively supported.
How does zod-to-openapi register a schema for reuse across routes?
You call `.openapi("<schema-name>")` on the schema, which adds it to components/schemas under that name and produces a `$ref` such as `#/components/schemas/User` where it is used. The third overload, `.openapi("<schema-name>", { ... })`, registers the schema and attaches extra metadata at the same time.
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/asteasolutions-zod-to-openapi)Star history chart for your README
[](https://hysenlabs.com/projects/asteasolutions-zod-to-openapi)Was this useful?