Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/asteasolutions/zod-to-openapi
A library that generates OpenAPI (Swagger) docs from Zod schemas
https://github.com/asteasolutions/zod-to-openapi
openapi openapi3 schema swagger zod
Last synced: 3 days ago
JSON representation
A library that generates OpenAPI (Swagger) docs from Zod schemas
- Host: GitHub
- URL: https://github.com/asteasolutions/zod-to-openapi
- Owner: asteasolutions
- License: mit
- Created: 2022-04-14T11:32:57.000Z (over 2 years ago)
- Default Branch: master
- Last Pushed: 2024-10-04T13:44:53.000Z (3 months ago)
- Last Synced: 2024-10-30T01:02:55.074Z (2 months ago)
- Topics: openapi, openapi3, schema, swagger, zod
- Language: TypeScript
- Homepage:
- Size: 859 KB
- Stars: 949
- Watchers: 10
- Forks: 59
- Open Issues: 21
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome - asteasolutions/zod-to-openapi - A library that generates OpenAPI (Swagger) docs from Zod schemas (TypeScript)
- awesome-zod - `zod-to-openapi` - Generate full OpenAPI (Swagger) docs from Zod, including schemas, endpoints & parameters. (Convertors and Generators)
README
# Zod to OpenAPI
[![npm version](https://img.shields.io/npm/v/@asteasolutions/zod-to-openapi)](https://www.npmjs.com/package/@asteasolutions/zod-to-openapi)
[![npm downloads](https://img.shields.io/npm/dm/@asteasolutions/zod-to-openapi)](https://www.npmjs.com/package/@asteasolutions/zod-to-openapi)A library that uses [zod schemas](https://github.com/colinhacks/zod) to generate an Open API Swagger documentation.
1. [Purpose and quick example](#purpose-and-quick-example)
2. [Usage](#usage)
1. [Installation](#installation)
2. [The `openapi` method](#the-openapi-method)
3. [The Registry](#the-registry)
4. [The Generator](#the-generator)
5. [Defining schemas](#defining-schemas)
6. [Defining routes & webhooks](#defining-routes--webhooks)
7. [Defining custom components](#defining-custom-components)
8. [A full example](#a-full-example)
9. [Adding it as part of your build](#adding-it-as-part-of-your-build)
10. [Using schemas vs a registry](#using-schemas-vs-a-registry)
3. [Zod schema types](#zod-schema-types)
1. [Supported types](#supported-types)
2. [Unsupported types](#unsupported-types)
4. [Technologies](#technologies)We keep a changelog as part of the [GitHub releases](https://github.com/asteasolutions/zod-to-openapi/releases).
## Purpose and quick example
We at [Astea Solutions](https://asteasolutions.com/) made this library because we use [zod](https://github.com/colinhacks/zod) for validation in our APIs and are tired of the duplication to also support a separate OpenAPI definition that must be kept in sync. Using `zod-to-openapi`, we generate OpenAPI definitions directly from our zod schemas, thus having a single source of truth.
Simply put, it turns this:
```ts
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');registry.registerPath({
method: 'get',
path: '/users/{id}',
summary: 'Get a single user',
request: {
params: z.object({ id: z.string() }),
},responses: {
200: {
description: 'Object with user data.',
content: {
'application/json': {
schema: UserSchema,
},
},
},
},
});
```into this:
```yaml
components:
schemas:
User:
type: object
properties:
id:
type: string
example: '1212121'
name:
type: string
example: John Doe
age:
type: number
example: 42
required:
- id
- name
- age/users/{id}:
get:
summary: Get a single user
parameters:
- in: path
name: id
schema:
type: string
required: true
responses:
'200':
description: Object with user data
content:
application/json:
schema:
$ref: '#/components/schemas/User'
```and you can still use `UserSchema` and the `request.params` object to validate the input of your API.
## Usage
### Installation
```shell
npm install @asteasolutions/zod-to-openapi
# or
yarn add @asteasolutions/zod-to-openapi
```### The `openapi` method
To keep openapi definitions natural, we add an `openapi` method to all Zod objects. Its idea is to provide a convenient way to provide OpenApi specific data.
It has three overloads:1. `.openapi({ [key]: value })` - this way we can specify any OpenApi fields. For example `z.number().openapi({ example: 3 })` would add `example: 3` to the generated schema.
2. `.openapi("")` - this way we specify that the underlying zod schema should be "registered" i.e added into `components/schemas` with the provided ``
3. `.openapi("", { [key]: value })` - this unites the two use cases above so that we can specify both a registration `` and additional metadataFor this to work, you need to call `extendZodWithOpenApi` once in your project.
This should be done only once in a common-entrypoint file of your project (for example an `index.ts`/`app.ts`). If you're using tree-shaking with Webpack, mark that file as having side-effects.
It can be bit tricky to achieve this in your codebase, because *require* is synchronous and *import* is a async.
### The basic idea
```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' });
```### Example 1: Calling the openapi-extension using tsx
```
//zod-extend.tsimport { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';extendZodWithOpenApi(z);
// package.json
"scripts": {
"start": "tsx --import ./zod-extend.ts ./index.ts",
```### Example 2 - require-syntax
```
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';extendZodWithOpenApi(z);
const { startServer } = require('./server/start');
startServer();
```### The Registry
The `OpenAPIRegistry` is a utility that can be used to collect definitions which would later be passed to a `OpenApiGeneratorV3` or `OpenApiGeneratorV31` instance.
```ts
import {
OpenAPIRegistry,
OpenApiGeneratorV3,
} from '@asteasolutions/zod-to-openapi';const registry = new OpenAPIRegistry();
// Register definitions here
const generator = new OpenApiGeneratorV3(registry.definitions);
return generator.generateComponents();
```### The Generator
There are two generators that can be used - `OpenApiGeneratorV3` and `OpenApiGeneratorV31`. They share the same interface but internally generate schemas that correctly follow the data format for the specific Open API version - `3.0.x` or `3.1.x`. The Open API version affects how some components are generated.
For example: changing the generator from `OpenApiGeneratorV3` to `OpenApiGeneratorV31` would result in following differences:
```ts
z.string().nullable().openapi(refId: 'name');
``````yml
# 3.1.0
# nullable is invalid in 3.1.0 but type arrays are invalid in previous versions
name:
type:
- 'string'
- 'null'# 3.0.0
name:
type: 'string'
nullable: true
```Both generators take a single argument in their constructors - an array of definitions - i.e results from the registry or regular zod schemas.
The public methods of both generators are as follows:
`generateComponents` will generate only the `/components` section of an OpenAPI document (e.g. only `schemas` and `parameters`), not generating actual routes.
`generateDocument` will generate the whole OpenAPI document.
### Defining schemas
An OpenApi schema should be registered by using the `.openapi` method and providing a name:
```ts
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');const generator = new OpenApiGeneratorV3([UserSchema]);
```The same can be achieved by using the `register` method of an `OpenAPIRegistry` instance. For more check the ["Using schemas vs a registry"](#using-schemas-vs-a-registry) section
```ts
const UserSchema = registry.register(
'User',
z.object({
id: z.string().openapi({ example: '1212121' }),
name: z.string().openapi({ example: 'John Doe' }),
age: z.number().openapi({ example: 42 }),
})
);const generator = new OpenApiGeneratorV3(registry.definitions);
```If run now, `generator.generateComponents()` will generate the following structure:
```yaml
components:
schemas:
User:
type: object
properties:
id:
type: string
example: '1212121'
name:
type: string
example: John Doe
age:
type: number
example: 42
required:
- id
- name
- age
```The key for the schema in the output is the first argument passed to `.openapi` method (or the `.register`) - in this case: `User`.
Note that `generateComponents` does not return YAML but a JS object - you can then serialize that object into YAML or JSON depending on your use-case.
The resulting schema can then be referenced by using `$ref: #/components/schemas/User` in an existing OpenAPI JSON. This will be done automatically for Routes defined through the registry.
Note by default a Zod object will result in `"additionalProperties": true` as per the Open API spec unless using `strict` or `catchall`, this is in contrast to normal Zod object usage where `zod.parse` is used.
### Defining routes & webhooks
#### Registering a path or webhook
An OpenAPI path is registered using the `registerPath` method of an `OpenAPIRegistry` instance. An OpenAPI webhook is registered using the `registerWebhook` method and takes the same parameters as `registerPath`.
```ts
registry.registerPath({
method: 'get',
path: '/users/{id}',
description: 'Get user data by its id',
summary: 'Get a single user',
request: {
params: z.object({
id: z.string().openapi({ example: '1212121' }),
}),
},
responses: {
200: {
description: 'Object with user data.',
content: {
'application/json': {
schema: UserSchema,
},
},
},
204: {
description: 'No content - successful operation',
},
},
});
```The YAML equivalent of the schema above would be:
```yaml
'/users/{id}':
get:
description: Get user data by its id
summary: Get a single user
parameters:
- in: path
name: id
schema:
type: string
example: '1212121'
required: true
responses:
'200':
description: Object with user data.
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'204':
description: No content - successful operation
```The library specific properties for `registerPath` are `method`, `path`, `request` and `responses`. Everything else gets directly appended to the path definition.
- `method` - One of `get`, `post`, `put`, `delete` and `patch`;
- `path` - a string - being the path of the endpoint;
- `request` - an optional object with optional `body`, `params`, `query` and `headers` keys,
- `query`, `params` - being instances of `ZodObject`
- `body` - an object with a `description` and a `content` record where:
- the key is a `mediaType` string like `application/json`
- and the value is an object with a `schema` of any `zod` type
- `headers` - instances of `ZodObject` or an array of any `zod` instances
- `responses` - an object where the key is the status code or `default` and the value is an object with a `description` and a `content` record where:
- the key is a `mediaType` string like `application/json`
- and the value is an object with a `schema` of any `zod` type#### Defining route parameters
If you don't want to inline all parameter definitions, you can define them separately with `registerParameter` and then reference them:
```ts
const UserIdParam = registry.registerParameter(
'UserId',
z.string().openapi({
param: {
name: 'id',
in: 'path',
},
example: '1212121',
})
);registry.registerPath({
...
request: {
params: z.object({
id: UserIdParam
}),
},
responses: ...
});
```The YAML equivalent would be:
```yaml
components:
parameters:
UserId:
in: path
name: id
schema:
type: string
example: '1212121'
required: true'/users/{id}':
get:
...
parameters:
- $ref: '#/components/parameters/UserId'
responses: ...
```Note: In order to define properties that apply to the parameter itself, use the `param` property of `.openapi`. Any properties provided outside of `param` would be applied to the schema for this parameter.
#### Generating the full document
A full OpenAPI document can be generated using the `generateDocument` method of an `OpenApiGeneratorV3` or `OpenApiGeneratorV31` instance. It takes one argument - the document config. It may look something like this:
```ts
return generator.generateDocument({
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'My API',
description: 'This is the API',
},
servers: [{ url: 'v1' }],
});
```### Defining custom components
You can define components that are not OpenAPI schemas, including security schemes, response headers and others. See [this test file](spec/custom-components.spec.ts) for examples.
### A full example
A full example code can be found [here](./example/index.ts). And the YAML representation of its result - [here](./example/openapi-docs.yml)
### Using schemas vs a registry
Schemas are automatically being registered when referenced. That means that if you have a schema like:
```ts
const schema = z.object({ key: z.string().openapi('Test') }).openapi('Object');
```you'd have the following resulting structure:
```yaml
components:
schemas:
Test:
type: 'string',
Object:
type: 'object',
properties:
key:
$ref: '#/components/schemas/Test'
required: ['key']
```This does not require any usages of an `OpenAPIRegistry` instance.
However the same output can be achieved with the following code:
```ts
const registry = new OpenAPIRegistry();
const schema = registry.register(
'Object',
z.object({ key: z.string().openapi('Test') })
);
```The main benefit of the `.registry` method is that you can use the registry as a "collection" where you would put all such schemas.
With `.openapi`:
```ts
// file1.ts
export const Schema1 = ...// file2.ts
export const Schema2 = ...new OpenApiGeneratorV3([Schema1, Schema2])
```Adding a `NewSchema` into `file3.ts` would require you to pass that schema manually into the array of the generator constructor.
Note: If a `NewSchema` is referenced by any other schemas or a route/webhook definition it would still appear in the resulting document.With `registry.register`:
```ts
// registry.ts
export const registry = new OpenAPIRegistry()// file1.ts
export const Schema1 = registry.register(...)// file2.ts
export const Schema2 = registry.register(...)new OpenApiGeneratorV3(registry.definitions)
```Adding a `NewSchema` into `file3.ts` and using `registry.register` would NOT require you to do any changes to the generator constructor.
#### Conclusion
Using an `OpenAPIRegistry` instance is mostly useful if you would want your resulting document to contain unreferenced schemas.
That can sometimes be useful - for example when you are slowly integrating an already existing documentation with `@asteasolutions/zod-to-openapi` and you are migrating small pieces at a time. Those pieces can then be referenced directly from an existing documentation.### Adding it as part of your build
In a file inside your project you can have a file like so:
```ts
export const registry = new OpenAPIRegistry();export function generateOpenAPI() {
const config = {...}; // your config comes herereturn new OpenApiGeneratorV3(registry.definitions).generateDocument(config);
}
```You then use the exported `registry` object to register all schemas, parameters and routes where appropriate.
Then you can create a script that executes the exported `generateOpenAPI` function. This script can be executed as a part of your build step so that it can write the result to some file like `openapi-docs.json`.
## Zod schema types
### Supported types
The list of all supported types as of now is:
- `ZodAny`
- `ZodArray`
- `ZodBigInt`
- `ZodBoolean`
- `ZodDate`
- `ZodDefault`
- `ZodDiscriminatedUnion`
- including `discriminator` mapping when all Zod objects in the union are registered with `.register()` or contain a `refId`.
- `ZodEffects`
- `ZodEnum`
- `ZodIntersection`
- `ZodLiteral`
- `ZodNativeEnum`
- `ZodNullable`
- `ZodNumber`
- including `z.number().int()` being inferred as `type: 'integer'`
- `ZodObject`
- including `.catchall` resulting in the respective `additionalProperties` schema
- also including `strict` resulting in the respective `additionalProperties` schema
- `ZodOptional`
- `ZodPipeline`
- `ZodReadonly`
- `ZodRecord`
- `ZodString`
- adding `format` for:
- `.emoji()`
- `.cuid()`
- `.cuid2()`
- `.ulid()`
- `.ip()`
- `.date()`
- `.datetime()`
- `.uuid()`
- `.email()`
- `.url()`
- adding `pattern` for `.regex()` is also supported${'emoji'} | ${z.string().emoji()} | ${'emoji'}
${'cuid'} | ${z.string().cuid()} | ${'cuid'}
${'cuid2'} | ${z.string().cuid2()} | ${'cuid2'}
${'ulid'} | ${z.string().ulid()} | ${'ulid'}
${'ip'} | ${z.string().ip()} | ${'ip'}- `ZodTuple`
- `ZodUnion`
- `ZodUnknown`Extending an instance of `ZodObject` is also supported and results in an OpenApi definition with `allOf`
### Unsupported types
In case you try to create an OpenAPI schema from a zod schema that is not one of the aforementioned types then you'd receive an `UnknownZodTypeError`.
You can still register such schemas on your own by providing a `type` via the `.openapi` method. In case you think that the desired behavior can be achieved automatically do not hesitate to reach out to us by describing your case via Github Issues.
#### Known issues
1. `z.nullable(schema)` [does not generate a $ref for underlying registered schemas](https://github.com/asteasolutions/zod-to-openapi/issues/141).
- This is an implementation limitation.
- However you can simply use `schema.nullable()` which has the exact same effect `zod` wise but it is also fully supported on our end.## Technologies
- [Typescript](https://www.typescriptlang.org/)
- [Zod 3.x](https://github.com/colinhacks/zod)
- [OpenAPI 3.x TS](https://github.com/metadevpro/openapi3-ts)