@asteasolutions/zod-to-openapi review
@asteasolutions/zod-to-openapi 9.1.0 converts Zod 4 schemas plus explicit route registrations into OpenAPI 3.0, 3.1, or 3.2 documents. It covers component schemas, parameters, request bodies, responses, webhooks, and security schemes. The current line includes an OpenAPI 3.2 generator while retaining separate generators for earlier dialects. Our install included declarations and loaded through both require() and ESM import. It does not inspect framework routes, validate a request for the handler, emit YAML, or prove the running API follows the generated contract.
@asteasolutions/zod-to-openapi 9.1.0 took 1.7 seconds and 9 MB in our sandbox, passed npm audit with 0 findings, and produced an 8.1 KB gzipped browser bundle. Install it for a Zod 4 API with explicit route registration; choose a framework generator if route discovery is the requirement.
We installed it
| Install | ✓ · 1.7s | 10 packages on disk · 9 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | 8.1 KB | gzipped (29.9 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does @asteasolutions/zod-to-openapi install cleanly?
Yes. In a fresh container with an empty cache, npm install @asteasolutions/zod-to-openapi finished in 2 seconds, leaving 10 packages and 9 MB on disk. npm audit reported no known vulnerabilities.
How much does @asteasolutions/zod-to-openapi add to a browser bundle?
8.1 KB gzipped (29.9 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does @asteasolutions/zod-to-openapi work with both ESM and CommonJS?
Yes. Both import '@asteasolutions/zod-to-openapi' and require('@asteasolutions/zod-to-openapi') worked in Node 22 in our run. The package is published as CommonJS.
Does @asteasolutions/zod-to-openapi include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
@asteasolutions/zod-to-openapi or zod-openapi: which should you use?
zod-openapi: Use it when its Zod 4 document-building API fits your codebase better. @asteasolutions/zod-to-openapi 9.1.0 took 1.7 seconds and 9 MB in our sandbox, passed npm audit with 0 findings, and produced an 8.1 KB gzipped browser bundle.
When should you not use @asteasolutions/zod-to-openapi?
Your application remains on Zod 3; the README sends that setup to 7.3.4 and says the older line is not actively supported
Use it if
- Zod 4 already validates your API data and maintaining separate OpenAPI schemas causes drift
- Your build can register paths, bodies, responses, webhooks, and components in one OpenAPIRegistry
- One schema source must generate OpenAPI 3.0, 3.1, or 3.2 without hand-editing dialect differences
- You can keep runtime validation and contract testing as explicit jobs outside the generator
- Your application remains on Zod 3; the README sends that setup to 7.3.4 and says the older line is not actively supported
- You want Express, Fastify, or filesystem routes discovered automatically; every operation must be registered in code
- Your schemas use unsupported Zod constructs without an OpenAPI override; generation can stop with UnknownZodTypeError
- You cannot guarantee extension-module execution order; .openapi() needs extendZodWithOpenApi(z), and tree shaking can discard an unmarked side-effect import
- You expect normal Zod object parsing rules in the schema output; the README says generated objects allow extra properties unless strict or catchall changes that
Setup reality
Our fresh Node 22 install of @asteasolutions/zod-to-openapi 9.1.0 finished in 1.7 seconds. It left 10 packages and 9 MB on disk, with one direct dependency, one Zod peer, and a 364 KB unpacked package. npm audit reported 0 known vulnerabilities. The package is CommonJS without an exports map, but require() and ESM import both worked. Declarations ship with it. Our full browser import measured 29.9 KB minified and 8.1 KB gzipped.
Install Zod 4 beside it. Simple schema IDs and examples can use Zod .meta(), while registered inheritance and parameter metadata still need extendZodWithOpenApi(z). That call must run before modules using .openapi(). A tree-shaken build also needs the extension entry marked as a side effect. The registry contains definitions only from modules that executed, so the generation script must import every route-registration module before constructing the document.
Choose OpenApiGeneratorV3, V31, or V32 for the consumer because nullability and document fields differ across the 3 versions. generateDocument() returns a JavaScript object, and YAML needs another serializer. Runtime parsing remains the web framework's job even when the handler imports the same Zod schema. CI should lint and diff the generated output because this package cannot detect a handler returning a different status or body.
Patterns
Load the extension before decorated schemas extend-zod-once
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';
extendZodWithOpenApi(z);One extendZodWithOpenApi(z) call adds .openapi() to the shared Zod instance. Execute this module before any decorated schema module.
Describe a schema through Zod 4 metadata use-zod-meta
import { z } from 'zod';
const UserId = z.string().uuid().meta({
id: 'UserId',
description: 'Stable user identifier',
example: '2f1f9a71-0c0b-4eb5-b4a4-bd27b90fb061',
});Zod 4 .meta() supplies an ID, description, and example without prototype extension. Parameter metadata and some inheritance cases still require .openapi().
Register a reusable Zod component register-component-schema
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';
const registry = new OpenAPIRegistry();
const User = registry.register('User', z.object({
id: z.string().uuid(),
name: z.string().min(1),
}));registry.register() returns the same Zod schema, so User can parse runtime data and appear as a referenced OpenAPI component.
Record a GET operation with a path value register-get-route
registry.registerPath({
method: 'get',
path: '/users/{id}',
summary: 'Get a user',
request: { params: z.object({ id: z.string().uuid() }) },
responses: {
200: {
description: 'User found',
content: { 'application/json': { schema: User } },
},
404: { description: 'User not found' },
},
});registerPath() writes documentation only. The server must still parse id and enforce the 200 and 404 response contracts.
Specify a required JSON request body register-post-body
const CreateUser = z.object({ name: z.string().min(1) });
registry.registerPath({
method: 'post',
path: '/users',
request: {
body: {
required: true,
content: { 'application/json': { schema: CreateUser } },
},
},
responses: {
201: {
description: 'User created',
content: { 'application/json': { schema: User } },
},
},
});The required flag belongs on the OpenAPI request-body object. Zod field rules describe the JSON carried inside it.
Share one registered path parameter register-shared-parameter
const UserId = registry.registerParameter(
'UserId',
z.string().uuid().openapi({
param: { name: 'id', in: 'path' },
description: 'User identifier',
}),
);
registry.registerPath({
method: 'delete',
path: '/users/{id}',
request: { params: z.object({ id: UserId }) },
responses: { 204: { description: 'Deleted' } },
});Parameter fields such as name and in belong under param. Schema description fields stay outside that nested object.
Apply a bearer security component add-security-scheme
registry.registerComponent('securitySchemes', 'bearerAuth', {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
});
registry.registerPath({
method: 'get',
path: '/me',
security: [{ bearerAuth: [] }],
responses: { 200: { description: 'Current user' } },
});Security schemes are raw OpenAPI components because a Zod value schema cannot express authentication policy.
Produce an OpenAPI 3.0 contract generate-openapi-30
import { OpenApiGeneratorV3 } from '@asteasolutions/zod-to-openapi';
const document = new OpenApiGeneratorV3(registry.definitions, {
sortComponents: 'alphabetically',
}).generateDocument({
openapi: '3.0.3',
info: { title: 'Users API', version: '1.0.0' },
servers: [{ url: 'https://api.example.com' }],
});OpenApiGeneratorV3 emits the 3.0 schema dialect. Alphabetical components keep generated JSON diffs stable.
Use OpenAPI 3.1 null semantics generate-openapi-31
import { OpenApiGeneratorV31 } from '@asteasolutions/zod-to-openapi';
const MaybeName = registry.register('MaybeName', z.string().nullable());
const document = new OpenApiGeneratorV31(registry.definitions)
.generateDocument({
openapi: '3.1.0',
info: { title: 'Example API', version: '1.0.0' },
});The 3.1 generator uses JSON Schema null types. OpenAPI 3.0 represents the same nullable Zod schema differently.
Describe stream items in OpenAPI 3.2 document-event-stream
import { OpenApiGeneratorV32 } from '@asteasolutions/zod-to-openapi';
const Event = registry.register('Event', z.object({ message: z.string() }));
registry.registerPath({
method: 'get',
path: '/events',
responses: {
200: {
description: 'Event stream',
content: { 'text/event-stream': { itemSchema: Event } },
},
},
});
const document = new OpenApiGeneratorV32(registry.definitions).generateDocument({
openapi: '3.2.0',
info: { title: 'Events API', version: '1.0.0' },
});itemSchema belongs to the OpenAPI 3.2 media-type shape. A 3.0 or 3.1 generator should not receive it.
Extract only the reusable components generate-components-only
import { OpenApiGeneratorV31 } from '@asteasolutions/zod-to-openapi';
const { components } = new OpenApiGeneratorV31(registry.definitions)
.generateComponents();generateComponents() returns a JavaScript component object without paths, suitable for merging into an existing 3.1 document.
Write JSON after all route imports write-document-build
import { writeFile } from 'node:fs/promises';
import './routes/users.js';
import { generateOpenAPI } from './openapi.js';
const document = generateOpenAPI();
await writeFile('./dist/openapi.json', JSON.stringify(document, null, 2) + '\n');Every module calling registry.registerPath() must execute before generation. JSON works directly; YAML requires another serializer.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| zod-openapi | npm | Use it when its Zod 4 document-building API fits your codebase better. |
| @anatine/zod-openapi | npm | Use it in an existing Anatine or NestJS-oriented Zod stack. |
| zod-to-json-schema | npm | Use it when JSON Schema is the target and paths will be assembled elsewhere. |
| tsoa | npm | Use it when controller types and decorators should generate routes as well as OpenAPI. |
More web backend guides
urllib3 · requests · ws · anyio · httpx · undici · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

