A Typescript library to use Zod Schemas to create OpenAPI v3.x documentation
Install via npm
, yarn
or pnpm
:
npm install zod zod-openapi
## or
yarn add zod zod-openapi
## or
pnpm install zod zod-openapi
This mutates Zod to add an extra .openapi()
method. Call this at the top of your entry point(s). You can achieve this in two different ways, depending on your preference.
import 'zod-openapi/extend';
import { z } from 'zod';
z.string().openapi({ description: 'hello world!', example: 'hello world' });
This is useful if you have a specific instance of Zod or a Zod instance from another library that you would like to target.
import { z } from 'zod';
import { extendZodWithOpenApi } from 'zod-openapi';
extendZodWithOpenApi(z);
z.string().openapi({ description: 'hello world!', example: 'hello world' });
.openapi()
Use the .openapi()
method to add metadata to a specific Zod type. The .openapi()
method takes an object with the following options:
Option | Description |
---|---|
OpenAPI Options | This will take any option you would put on a SchemaObject. |
effectType |
Use to override the creation type for a Zod Effect |
header |
Use to provide metadata for response headers |
param |
Use to provide metadata for request parameters |
ref |
Use this to auto register a schema as a re-usable component |
refType |
Use this to set the creation type for a component which is not referenced in the document. |
type |
Use this to override the generated type. If this is provided no metadata will be generated. |
unionOneOf |
Set to true to force a single ZodUnion to output oneOf instead of anyOf . See CreateDocumentOptions for a global option |
createDocument
Creates an OpenAPI documentation object
import 'zod-openapi/extend';
import { z } from 'zod';
import { createDocument } from 'zod-openapi';
const jobId = z.string().openapi({
description: 'A unique identifier for a job',
example: '12345',
ref: 'jobId',
});
const title = z.string().openapi({
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 }) },
},
},
},
},
},
},
});
{
"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 CreateDocumentOptions
argument which can be used to modify how the document is created.
const document = createDocument(details, {
defaultDateSchema: { type: 'string', format: 'date-time' }, // defaults to { type: 'string' }
unionOneOf: true, // defaults to false. Forces all ZodUnions to output oneOf instead of anyOf. An `.openapi()` `unionOneOf` value takes precedence over this one.
});
createSchema
Creates an OpenAPI Schema Object along with any registered components. OpenAPI 3.1.0 Schema Objects are fully compatible with JSON Schema.
import 'zod-openapi/extend';
import { z } from 'zod';
import { createSchema } from 'zod-openapi';
const jobId = z.string().openapi({
description: 'A unique identifier for a job',
example: '12345',
ref: 'jobId',
});
const title = z.string().openapi({
description: 'Job title',
example: 'My job',
});
const job = z.object({
jobId,
title,
});
const { schema, components } = createSchema(job);
{
"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"
}
}
}
createSchema
takes an optional CreateSchemaOptions
parameter which can also take the same options as CreateDocumentOptions along with the following options:
const { schema, components } = createSchema(job, {
schemaType: 'input'; // This controls whether this should be rendered as a request (`input`) or response (`output`). Defaults to `output`
openapi: '3.0.0'; // OpenAPI version to use, defaults to `'3.1.0'`
components: { jobId: z.string() } // Additional components to use and create while rendering the schema
componentRefPath: '#/definitions/' // Defaults to #/components/schemas/
})
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().openapi({
param: {
name: 'job-header',
in: 'header',
},
}),
],
},
},
},
});
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.
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(),
}),
},
},
},
},
},
});
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() }),
},
},
},
},
},
},
},
},
},
},
},
});
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
const title = z.string().openapi({
description: 'Job title',
example: 'My job',
ref: '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 can be an extremely powerful 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.
createDocument({
components: {
schemas: {
jobTitle: title, // this will register this Zod Schema as jobTitle unless `ref` in `.openapi()` is specified on the type
},
},
});
Unfortunately, as a limitation of this library, you will need to attach an .openapi()
field or .describe()
to the schema that you are passing into the components or else you may not get the full power of the component generation. As a result, I recommend utilising the auto registering components over manual registration.
Query, Path, Header & Cookie parameters can be similarly registered:
// Easy auto registration
const jobId = z.string().openapi({
description: 'Job ID',
example: '1234',
param: { ref: 'jobRef' },
});
createDocument({
paths: {
'/jobs/{jobId}': {
put: {
requestParams: {
header: z.object({
jobId,
}),
},
},
},
},
});
// or more verbose auto registration
const jobId = z.string().openapi({
description: 'Job ID',
example: '1234',
param: { in: 'header', name: 'jobId', ref: 'jobRef' },
});
createDocument({
paths: {
'/jobs/{jobId}': {
put: {
parameters: [jobId],
},
},
},
});
// or manual registeration
const otherJobId = z.string().openapi({
description: 'Job ID',
example: '1234',
param: { in: 'header', name: 'jobId' },
});
createDocument({
components: {
parameters: {
jobRef: jobId,
},
},
});
Response headers can be similarly registered:
const header = z.string().openapi({
description: 'Job ID',
example: '1234',
header: { ref: 'some-header' },
});
// or
const jobIdHeader = z.string().openapi({
description: 'Job ID',
example: '1234',
});
createDocument({
components: {
headers: {
someHeaderRef: jobIdHeader,
},
},
});
Entire Responses can also be registered
const response: ZodOpenApiResponseObject = {
description: '200 OK',
content: {
'application/json': {
schema: z.object({ a: z.string() }),
},
},
ref: '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 can also be registered
const callback: ZodOpenApiCallbackObject = {
ref: 'some-callback'
post: {
responses: {
200: {
description: '200 OK',
content: {
'application/json': {
schema: z.object({ a: z.string() }),
}