Gathering detailed insights and metrics for zod-openapi
Gathering detailed insights and metrics for zod-openapi
Gathering detailed insights and metrics for zod-openapi
Gathering detailed insights and metrics for zod-openapi
@asteasolutions/zod-to-openapi
Builds OpenAPI schemas from Zod schemas
@anatine/zod-openapi
Zod to OpenAPI converter
@hono/zod-openapi
A wrapper class of Hono which supports OpenAPI.
openapi-zod-client
[](https://openapi-zod-client.vercel.app/)
Use Zod Schemas to create OpenAPI v3.x documentation
npm install zod-openapi
Typescript
Module System
Min. Node Version
Node Version
NPM Version
99.6
Supply Chain
100
Quality
86.7
Maintenance
100
Vulnerability
100
License
TypeScript (99.67%)
JavaScript (0.33%)
Total Downloads
3,294,218
Last Day
20,939
Last Week
113,013
Last Month
461,967
Last Year
2,965,548
MIT License
410 Stars
531 Commits
19 Forks
3 Watchers
12 Branches
10 Contributors
Updated on May 04, 2025
Minified
Minified + Gzipped
Latest Version
4.2.4
Package Id
zod-openapi@4.2.4
Unpacked Size
227.09 kB
Size
41.71 kB
File Count
40
NPM Version
10.9.2
Node Version
22.14.0
Published on
Mar 29, 2025
Cumulative downloads
Total Downloads
Last Day
-6.1%
20,939
Compared to previous day
Last Week
-7.4%
113,013
Compared to previous week
Last Month
3.6%
461,967
Compared to previous month
Last Year
810.2%
2,965,548
Compared to previous year
1
A TypeScript library for using Zod schemas to generate OpenAPI v3.x documentation.
Install via npm
, yarn
, or pnpm
:
1npm install zod zod-openapi 2# or 3yarn add zod zod-openapi 4# or 5pnpm install zod zod-openapi
This package extends Zod by adding an .openapi()
method. Call this at the top of your entry point(s). You can apply this extension in two ways:
1import 'zod-openapi/extend'; 2import { z } from 'zod'; 3 4z.string().openapi({ description: 'Hello world!', example: 'Hello world' });
This method is useful if you have a specific instance of Zod or a Zod instance from another library that you want to extend.
1import { z } from 'zod'; 2import { extendZodWithOpenApi } from 'zod-openapi'; 3 4extendZodWithOpenApi(z); 5 6z.string().openapi({ description: 'Hello world!', example: 'hello world' });
.openapi()
Use the .openapi()
method to add metadata to a Zod schema. It accepts an object with the following options:
Option | Description |
---|---|
OpenAPI Options | Accepts any option available for a SchemaObject. |
effectType | Overrides the creation type for a Zod Effect. |
header | Adds metadata for response headers. |
param | Adds metadata for request parameters. |
ref | Registers a schema as a reusable OpenAPI component. |
refType | Defines the creation type for a component not explicitly referenced in the document. |
type | Overrides the generated type. If provided, no metadata will be generated. |
unionOneOf | Forces a oneOf instead of anyOf for unions. See CreateDocumentOptions for a global setting. |
createDocument
Generates an OpenAPI documentation object.
1import 'zod-openapi/extend'; 2import { z } from 'zod'; 3import { createDocument } from 'zod-openapi'; 4 5const jobId = z.string().openapi({ 6 description: 'A unique identifier for a job', 7 example: '12345', 8 ref: 'jobId', 9}); 10 11const title = z.string().openapi({ 12 description: 'Job title', 13 example: 'My job', 14}); 15 16const document = createDocument({ 17 openapi: '3.1.0', 18 info: { 19 title: 'My API', 20 version: '1.0.0', 21 }, 22 paths: { 23 '/jobs/{jobId}': { 24 put: { 25 requestParams: { path: z.object({ jobId }) }, 26 requestBody: { 27 content: { 28 'application/json': { schema: z.object({ title }) }, 29 }, 30 }, 31 responses: { 32 '200': { 33 description: '200 OK', 34 content: { 35 'application/json': { schema: z.object({ jobId, title }) }, 36 }, 37 }, 38 }, 39 }, 40 }, 41 }, 42});
1{ 2 "openapi": "3.1.0", 3 "info": { 4 "title": "My API", 5 "version": "1.0.0" 6 }, 7 "paths": { 8 "/jobs/{jobId}": { 9 "put": { 10 "parameters": [ 11 { 12 "in": "path", 13 "name": "jobId", 14 "description": "A unique identifier for a job", 15 "schema": { 16 "$ref": "#/components/schemas/jobId" 17 } 18 } 19 ], 20 "requestBody": { 21 "content": { 22 "application/json": { 23 "schema": { 24 "type": "object", 25 "properties": { 26 "title": { 27 "type": "string", 28 "description": "Job title", 29 "example": "My job" 30 } 31 }, 32 "required": ["title"] 33 } 34 } 35 } 36 }, 37 "responses": { 38 "200": { 39 "description": "200 OK", 40 "content": { 41 "application/json": { 42 "schema": { 43 "type": "object", 44 "properties": { 45 "jobId": { 46 "$ref": "#/components/schemas/jobId" 47 }, 48 "title": { 49 "type": "string", 50 "description": "Job title", 51 "example": "My job" 52 } 53 }, 54 "required": ["jobId", "title"] 55 } 56 } 57 } 58 } 59 } 60 } 61 } 62 }, 63 "components": { 64 "schemas": { 65 "jobId": { 66 "type": "string", 67 "description": "A unique identifier for a job", 68 "example": "12345" 69 } 70 } 71 } 72}
createDocument
takes an optional CreateDocumentOptions
argument which can be used to modify how the document is created.
1const document = createDocument(details, { 2 defaultDateSchema: { type: 'string', format: 'date-time' }, // defaults to { type: 'string' } 3 unionOneOf: true, // defaults to false. Forces all ZodUnions to output oneOf instead of anyOf. An `.openapi()` `unionOneOf` value takes precedence over this one. 4 enforceDiscriminatedUnionComponents: true, // defaults to false. Throws an error if a Discriminated Union member is not registered as a component. 5});
createSchema
Creates an OpenAPI Schema Object along with any registered components. OpenAPI 3.1.0 Schema Objects are fully compatible with JSON Schema.
1import 'zod-openapi/extend'; 2import { z } from 'zod'; 3import { createSchema } from 'zod-openapi'; 4 5const jobId = z.string().openapi({ 6 description: 'A unique identifier for a job', 7 example: '12345', 8 ref: 'jobId', 9}); 10 11const title = z.string().openapi({ 12 description: 'Job title', 13 example: 'My job', 14}); 15 16const job = z.object({ 17 jobId, 18 title, 19}); 20 21const { schema, components } = createSchema(job);
1{ 2 "schema": { 3 "type": "object", 4 "properties": { 5 "jobId": { 6 "$ref": "#/components/schemas/jobId" 7 }, 8 "title": { 9 "type": "string", 10 "description": "Job title", 11 "example": "My job" 12 } 13 }, 14 "required": ["jobId", "title"] 15 }, 16 "components": { 17 "jobId": { 18 "type": "string", 19 "description": "A unique identifier for a job", 20 "example": "12345" 21 } 22 } 23}
createSchema
takes an optional CreateSchemaOptions
parameter which can also take the same options as CreateDocumentOptions along with the following options:
1const { schema, components } = createSchema(job, { 2 schemaType: 'input'; // This controls whether this should be rendered as a request (`input`) or response (`output`). Defaults to `output` 3 openapi: '3.0.0'; // OpenAPI version to use, defaults to `'3.1.0'` 4 components: { jobId: z.string() } // Additional components to use and create while rendering the schema 5 componentRefPath: '#/definitions/' // Defaults to #/components/schemas/ 6})
Query, Path, Header & Cookie parameters can be created using the requestParams
key under the method
key as follows:
1createDocument({ 2 paths: { 3 '/jobs/{a}': { 4 put: { 5 requestParams: { 6 path: z.object({ a: z.string() }), 7 query: z.object({ b: z.string() }), 8 cookie: z.object({ cookie: z.string() }), 9 header: z.object({ 'custom-header': z.string() }), 10 }, 11 }, 12 }, 13 }, 14});
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.
1createDocument({ 2 paths: { 3 '/jobs/{a}': { 4 put: { 5 parameters: [ 6 z.string().openapi({ 7 param: { 8 name: 'job-header', 9 in: 'header', 10 }, 11 }), 12 ], 13 }, 14 }, 15 }, 16});
Where you would normally declare the media type, set the schema
as your Zod Schema as follows.
1createDocument({ 2 paths: { 3 '/jobs': { 4 get: { 5 requestBody: { 6 content: { 7 'application/json': { schema: z.object({ a: z.string() }) }, 8 }, 9 }, 10 }, 11 }, 12 }, 13});
If you wish to use OpenAPI syntax for your schemas, simply add an OpenAPI schema to the schema
field instead.
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.
1createDocument({ 2 paths: { 3 '/jobs': { 4 get: { 5 responses: { 6 200: { 7 description: '200 OK', 8 content: { 9 'application/json': { schema: z.object({ a: z.string() }) }, 10 }, 11 headers: z.object({ 12 'header-key': z.string(), 13 }), 14 }, 15 }, 16 }, 17 }, 18 }, 19});
1createDocument({ 2 paths: { 3 '/jobs': { 4 get: { 5 callbacks: { 6 onData: { 7 '{$request.query.callbackUrl}/data': { 8 post: { 9 requestBody: { 10 content: { 11 'application/json': { schema: z.object({ a: z.string() }) }, 12 }, 13 }, 14 responses: { 15 200: { 16 description: '200 OK', 17 content: { 18 'application/json': { 19 schema: z.object({ a: z.string() }), 20 }, 21 }, 22 }, 23 }, 24 }, 25 }, 26 }, 27 }, 28 }, 29 }, 30 }, 31});
OpenAPI allows you to define reusable components and this library allows you to replicate that in two separate ways.
If we take the example in createDocument
and instead create title
as follows
1const title = z.string().openapi({ 2 description: 'Job title', 3 example: 'My job', 4 ref: 'jobTitle', // <- new field 5});
Wherever title
is used in schemas across the document, it will instead be created as a reference.
1{ "$ref": "#/components/schemas/jobTitle" }
title
will then be outputted as a schema within the components section of the documentation.
1{ 2 "components": { 3 "schemas": { 4 "jobTitle": { 5 "type": "string", 6 "description": "Job title", 7 "example": "My job" 8 } 9 } 10 } 11}
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.
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.
1const title = z.string().openapi({ 2 description: 'Job title', 3 example: 'My job', 4}); 5createDocument({ 6 components: { 7 schemas: { 8 jobTitle: title, // this will register this Zod Schema as jobTitle unless `ref` in `.openapi()` is specified on the type 9 }, 10 }, 11});
Unfortunately, as a limitation of this library, you should attach an .openapi()
field or .describe()
to the schema that you are passing into the components or you will not reap the full benefits of component generation.
1// ❌ Avoid this 2const title = z.string(); 3 4// ✅ Recommended ways 5const title = z.string().describe('Job title'); 6const title = z.string().openapi({ 7 description: 'Job title', 8 example: 'My job', 9}); 10 11createDocument({ 12 components: { 13 schemas: { 14 jobTitle: title, 15 }, 16 }, 17});
Overall, I recommend utilising the auto registering components over manual registration.
Query, Path, Header & Cookie parameters can be similarly registered:
1// Easy auto registration 2const jobId = z.string().openapi({ 3 description: 'Job ID', 4 example: '1234', 5 param: { ref: 'jobRef' }, 6}); 7 8createDocument({ 9 paths: { 10 '/jobs/{jobId}': { 11 put: { 12 requestParams: { 13 header: z.object({ 14 jobId, 15 }), 16 }, 17 }, 18 }, 19 }, 20}); 21 22// or more verbose auto registration 23const jobId = z.string().openapi({ 24 description: 'Job ID', 25 example: '1234', 26 param: { in: 'header', name: 'jobId', ref: 'jobRef' }, 27}); 28 29createDocument({ 30 paths: { 31 '/jobs/{jobId}': { 32 put: { 33 parameters: [jobId], 34 }, 35 }, 36 }, 37}); 38 39// or manual registeration 40const otherJobId = z.string().openapi({ 41 description: 'Job ID', 42 example: '1234', 43 param: { in: 'header', name: 'jobId' }, 44}); 45 46createDocument({ 47 components: { 48 parameters: { 49 jobRef: jobId, 50 }, 51 }, 52});
Response headers can be similarly registered:
1const header = z.string().openapi({ 2 description: 'Job ID', 3 example: '1234', 4 header: { ref: 'some-header' }, 5}); 6 7// or 8 9const jobIdHeader = z.string().openapi({ 10 description: 'Job ID', 11 example: '1234', 12}); 13 14createDocument({ 15 components: { 16 headers: { 17 someHeaderRef: jobIdHeader, 18 }, 19 }, 20});
Entire Responses can also be registered
1const response: ZodOpenApiResponseObject = { 2 description: '200 OK', 3 content: { 4 'application/json': { 5 schema: z.object({ a: z.string() }), 6 }, 7 }, 8 ref: 'some-response', 9}; 10 11//or 12 13const response: ZodOpenApiResponseObject = { 14 description: '200 OK', 15 content: { 16 'application/json': { 17 schema: z.object({ a: z.string() }), 18 }, 19 }, 20}; 21 22createDocument({ 23 components: { 24 responses: { 25 'some-response': response, 26 }, 27 }, 28});
Callbacks can also be registered
1const callback: ZodOpenApiCallbackObject = { 2 ref: 'some-callback' 3 post: { 4 responses: { 5 200: { 6 description: '200 OK', 7 content: { 8 'application/json': { 9 schema: z.object({ a: z.string() }), 10 }, 11 }, 12 }, 13 }, 14 }, 15}; 16 17//or 18 19const callback: ZodOpenApiCallbackObject = { 20 post: { 21 responses: { 22 200: { 23 description: '200 OK', 24 content: { 25 'application/json': { 26 schema: z.object({ a: z.string() }), 27 }, 28 }, 29 }, 30 }, 31 }, 32}; 33 34createDocument({ 35 components: { 36 callbacks: { 37 'some-callback': callback, 38 }, 39 }, 40});
.transform()
, .catch()
, .default()
and .pipe()
are complicated because they all comprise of two different types that we could generate (input & output).
We attempt to determine what type of schema to create based on the following contexts:
Input: Request Bodies, Request Parameters, Headers
Output: Responses, Response Headers
As an example:
1z.object({ 2 a: z.string().default('a'), 3});
In a request context, this would render the following OpenAPI schema:
1type: 'object' 2properties: 3 - a: 4 type: 'string' 5 default: 'a'
or the following for a response:
1type: 'object' 2properties: 3 - a: 4 type: 'string' 5 default: 'a' 6required: 7 - a
Note how the response schema created an extra required
field. This means, if you were to register a Zod schema with .default()
as a component and use it in both a request or response, your schema would be invalid. Zod OpenAPI keeps track of this usage and will throw an error if this occurs.
1z.string().transform((str) => str.trim());
Whilst the TypeScript compiler can understand that the result is still a string
, unfortunately we cannot introspect this as your transform function may be far more complicated than this example. To address this, you can set the effectType
on the schema to same
, input
or output
.
same
- This informs Zod OpenAPI to pick either the input schema or output schema to generate with because they should be the same.
1z.string() 2 .transform((str) => str.trim()) 3 .openapi({ effectType: 'same' });
If the transform were to drift from this, you will receive a TypeScript error:
1z.string() 2 .transform((str) => str.length) 3 .openapi({ effectType: 'same' }); 4// ~~~~~~~~~~ 5// Type 'same' is not assignable to type 'CreationType | undefined'.ts(2322)
input
or output
- This tells Zod OpenAPI to pick a specific schema to create whenever we run into this schema, regardless of it is a request or response schema.
1z.string() 2 .transform((str) => str.length) 3 .openapi({ effectType: 'input' });
.preprocess()
will always return the output
type even if we are creating an input schema. If a different input type is required you can achieve this with a .transform()
combined with a .pipe()
or simply declare a manual type
in .openapi()
.
If you are adding a ZodSchema directly to the components
section which is not referenced anywhere in the document, additional context may be required to create either an input or output schema. You can do this by setting the refType
field to input
or output
in .openapi()
. This defaults to output
by default.
Currently the following versions of OpenAPI are supported
3.0.0
3.0.1
3.0.2
3.0.3
3.1.0
Setting the openapi
field will change how the some of the components are rendered.
1createDocument({ 2 openapi: '3.1.0', 3});
As an example z.string().nullable()
will be rendered differently
3.0.0
1{ 2 "type": "string", 3 "nullable": true 4}
3.1.0
1{ 2 "type": ["string", "null"] 3}
minItems
/maxItems
mapping for .length()
, .min()
, .max()
integer
type
and int64
format
mappingtype
is mapped as string
by defaultdiscriminator
mapping when all schemas in the union are registered. The discriminator must be a ZodLiteral
, ZodEnum
or ZodNativeEnum
with string values. Only values wrapped in ZodBranded
, ZodReadOnly
and ZodCatch
are supported.transform
support for request schemas. See Zod Effects for how to enable response schema supportpre-process
support. We assume that the input type is the same as the output type. Otherwise pipe and transform can be used instead.refine
full supportstring
, number
and combined enums.integer
type
mapping for .int()
exclusiveMin
/min
/exclusiveMax
/max
mapping for .min()
, .max()
, lt()
, gt()
, .positive()
, .negative()
, .nonnegative()
, .nonpositive()
.multipleOf
mapping for .multipleOf()
additionalProperties
mapping for .catchall()
, .strict()
allOf
mapping for .extend()
when the base object is registered and does not have catchall()
, strict()
and extension does not override a field.uniqueItems
(you may need to add a pre-process to convert it to a set)format
mapping for .url()
, .uuid()
, .email()
, .datetime()
, .date()
, .time()
, .duration()
, .ip({ version: 'v4' })
, .ip({ version: 'v6' })
, .cidr({ version: 'v4' })
, .cidr({ version: 'v6' })
minLength
/maxLength
mapping for .length()
, .min()
, .max()
pattern
mapping for .regex()
, .startsWith()
, .endsWith()
, .includes()
contentEncoding
mapping for .base64()
for OpenAPI 3.1.0+items
mapping for .rest()
prefixItems
mapping for OpenAPI 3.1.0+anyOf
schema. Use unionOneOf
to change this to output oneOf
instead.If this library cannot determine a type for a Zod Schema, it will throw an error. To avoid this, declare a manual type
in the .openapi()
section of that schema.
eg.
1z.custom().openapi({ type: 'string' });
See the library in use in the examples folder.
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.
1pnpm 2pnpm build
1pnpm test
1# Fix issues 2pnpm format 3 4# Check for issues 5pnpm lint
To release a new version
🏷️ Choose a tag
, enter a version number. eg. v1.2.0
and click + Create new tag: vX.X.X on publish
.Generate release notes
button and adjust the description.Set as the latest release
box and click Publish release
. This will trigger the Release
workflow.Pull Requests
tab for a PR labelled Release vX.X.X
.Merge Pull Request
on that Pull Request to update master with the new package version.To release a new beta version
🏷️ Choose a tag
, enter a version number with a -beta.X
suffix eg. v1.2.0-beta.1
and click + Create new tag: vX.X.X-beta.X on publish
.Generate release notes
button and adjust the description.Set as a pre-release
box and click Publish release
. This will trigger the Prerelease
workflow.No vulnerabilities found.
No security vulnerabilities found.