Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/samchungy/zod-openapi
Use Zod Schemas to create OpenAPI v3.x documentation
https://github.com/samchungy/zod-openapi
json-schema openapi openapi3 swagger typescript zod zod-openapi
Last synced: 1 day ago
JSON representation
Use Zod Schemas to create OpenAPI v3.x documentation
- Host: GitHub
- URL: https://github.com/samchungy/zod-openapi
- Owner: samchungy
- License: mit
- Created: 2023-04-15T06:28:51.000Z (over 1 year ago)
- Default Branch: master
- Last Pushed: 2024-12-02T23:19:08.000Z (11 days ago)
- Last Synced: 2024-12-06T18:50:33.256Z (7 days ago)
- Topics: json-schema, openapi, openapi3, swagger, typescript, zod, zod-openapi
- Language: TypeScript
- Homepage:
- Size: 2.03 MB
- Stars: 306
- Watchers: 2
- Forks: 15
- Open Issues: 6
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
- Codeowners: .github/CODEOWNERS
Awesome Lists containing this project
README
zod-openapi
A Typescript library to use Zod Schemas to create OpenAPI v3.x documentation
## Install
Install via `npm`, `yarn` or `pnpm`:
```bash
npm install zod zod-openapi
## or
yarn add zod zod-openapi
## or
pnpm install zod zod-openapi
```## Usage
### Extend Zod
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.
#### Subpath Import
```ts
import 'zod-openapi/extend';
import { z } from 'zod';z.string().openapi({ description: 'hello world!', example: 'hello world' });
```#### Manual Extension
This is useful if you have a specific instance of Zod or a Zod instance from another library that you would like to target.
```typescript
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](https://swagger.io/docs/specification/data-models/data-types/). |
| `effectType` | Use to override the creation type for a [Zod Effect](#zod-effects) |
| `header` | Use to provide metadata for [response headers](#response-headers) |
| `param` | Use to provide metadata for [request parameters](#parameters) |
| `ref` | Use this to [auto register a schema as a re-usable component](#creating-components) |
| `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](#CreateDocumentOptions) for a global option |### `createDocument`
Creates an OpenAPI documentation object
```typescript
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 }) },
},
},
},
},
},
},
});
```Creates the following object:
```json
{
"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"
}
}
}
}
```#### CreateDocumentOptions
`createDocument` takes an optional `CreateDocumentOptions` argument which can be used to modify how the document is created.
```typescript
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.
enforceDiscriminatedUnionComponents: true, // defaults to false. Throws an error if a Discriminated Union member is not registered as a component.
});
```### `createSchema`
Creates an OpenAPI Schema Object along with any registered components. OpenAPI 3.1.0 Schema Objects are fully compatible with JSON Schema.
```typescript
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);
```Creates the following object:
```json
{
"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"
}
}
}
```#### CreateSchemaOptions
`createSchema` takes an optional `CreateSchemaOptions` parameter which can also take the same options as [CreateDocumentOptions](#createdocumentoptions) along with the following options:
```typescript
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/
})
```### Request Parameters
Query, Path, Header & Cookie parameters can be created using the `requestParams` key under the `method` key as follows:
```typescript
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](https://swagger.io/docs/specification/describing-parameters/) key. The definitions will then all be combined.
```ts
createDocument({
paths: {
'/jobs/{a}': {
put: {
parameters: [
z.string().openapi({
param: {
name: 'job-header',
in: 'header',
},
}),
],
},
},
},
});
```### Request Body
Where you would normally declare the [media type](https://swagger.io/docs/specification/media-types/), set the `schema` as your Zod Schema as follows.
```typescript
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.
### Responses
Similarly to the [Request Body](#request-body), simply set the `schema` as your Zod Schema as follows. You can set the response headers using the `headers` key.
```typescript
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(),
}),
},
},
},
},
},
});
```### Callbacks
```typescript
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() }),
},
},
},
},
},
},
},
},
},
},
},
});
```### Creating Components
OpenAPI allows you to define reusable [components](https://swagger.io/docs/specification/components/) and this library allows you to replicate that in two separate ways.
1. Auto registering schema
2. Manually registering schema#### Schema
If we take the example in `createDocument` and instead create `title` as follows
##### Auto Registering Schema
```typescript
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.
```json
{ "$ref": "#/components/schemas/jobTitle" }
````title` will then be outputted as a schema within the components section of the documentation.
```json
{
"components": {
"schemas": {
"jobTitle": {
"type": "string",
"description": "Job title",
"example": "My job"
}
}
}
}
```This is a great way to create less repetitive Open API documentation. There are some Open API features like [discriminator mapping](https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/) which require all schemas in the union to contain a ref.
##### Manually Registering Schema
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.
```typescript
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 you will not reap the full benefits of component generation. As a result, I recommend utilising the auto registering components over manual registration.
#### Parameters
Query, Path, Header & Cookie parameters can be similarly registered:
```typescript
// 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
Response headers can be similarly registered:
```typescript
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,
},
},
});
```#### Responses
Entire Responses can also be registered
```typescript
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
Callbacks can also be registered
```typescript
const callback: ZodOpenApiCallbackObject = {
ref: 'some-callback'
post: {
responses: {
200: {
description: '200 OK',
content: {
'application/json': {
schema: z.object({ a: z.string() }),
},
},
},
},
},
};//or
const callback: ZodOpenApiCallbackObject = {
post: {
responses: {
200: {
description: '200 OK',
content: {
'application/json': {
schema: z.object({ a: z.string() }),
},
},
},
},
},
};createDocument({
components: {
callbacks: {
'some-callback': callback,
},
},
});
```### Zod Effects
`.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:
```ts
z.object({
a: z.string().default('a'),
});
```In a request context, this would render the following OpenAPI schema:
```yaml
type: 'object'
properties:
- a:
type: 'string'
default: 'a'
```or the following for a response:
```yaml
type: 'object'
properties:
- a:
type: 'string'
default: 'a'
required:
- 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.
#### EffectType
```ts
z.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.
```ts
z.string()
.transform((str) => str.trim())
.openapi({ effectType: 'same' });
```If the transform were to drift from this, you will receive a TypeScript error:
```ts
z.string()
.transform((str) => str.length)
.openapi({ effectType: 'same' });
// ~~~~~~~~~~
// 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.
```ts
z.string()
.transform((str) => str.length)
.openapi({ effectType: 'input' });
```#### Preprocess
`.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()`.
#### Component Effects
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.
## Supported OpenAPI Versions
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.
```ts
createDocument({
openapi: '3.1.0',
});
```As an example `z.string().nullable()` will be rendered differently
`3.0.0`
```json
{
"type": "string",
"nullable": true
}
````3.1.0`
```json
{
"type": ["string", "null"]
}
```## Supported Zod Schema
- ZodAny
- ZodArray
- `minItems`/`maxItems` mapping for `.length()`, `.min()`, `.max()`
- ZodBigInt
- `integer` `type` and `int64` `format` mapping
- ZodBoolean
- ZodBranded
- ZodCatch
- Treated as ZodDefault
- ZodCustom
- ZodDate
- `type` is mapped as `string` by default
- ZodDefault
- ZodDiscriminatedUnion
- `discriminator` mapping when all schemas in the union are [registered](#creating-components). The discriminator must be a `ZodLiteral`, `ZodEnum` or `ZodNativeEnum` with string values. Only values wrapped in `ZodBranded`, `ZodReadOnly` and `ZodCatch` are supported.
- ZodEffects
- `transform` support for request schemas. See [Zod Effects](#zod-effects) for how to enable response schema support
- `pre-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 support
- ZodEnum
- ZodIntersection
- ZodLazy
- The recursive schema within the ZodLazy or the ZodLazy _**must**_ be registered as a component. See [Creating Components](#creating-components) for more information.
- ZodLiteral
- ZodNativeEnum
- supporting `string`, `number` and combined enums.
- ZodNever
- ZodNull
- ZodNullable
- ZodNumber
- `integer` `type` mapping for `.int()`
- `exclusiveMin`/`min`/`exclusiveMax`/`max` mapping for `.min()`, `.max()`, `lt()`, `gt()`, `.positive()`, `.negative()`, `.nonnegative()`, `.nonpositive()`.
- `multipleOf` mapping for `.multipleOf()`
- ZodObject
- `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.
- ZodOptional
- ZodPipeline
- See [Zod Effects](#zod-effects) for more information.
- ZodReadonly
- ZodRecord
- ZodSet
- Treated as an array with `uniqueItems` (you may need to add a pre-process to convert it to a set)
- ZodString
- `format` mapping for `.url()`, `.uuid()`, `.email()`, `.datetime()`, `.date()`, `.time()`, `.duration()`
- `minLength`/`maxLength` mapping for `.length()`, `.min()`, `.max()`
- `pattern` mapping for `.regex()`, `.startsWith()`, `.endsWith()`, `.includes()`
- `contentEncoding` mapping for `.base64()` for OpenAPI 3.1.0+
- ZodTuple
- `items` mapping for `.rest()`
- `prefixItems` mapping for OpenAPI 3.1.0+
- ZodUndefined
- ZodUnion
- By default it outputs an `anyOf` schema. Use `unionOneOf` to change this to output `oneOf` instead.
- ZodUnknownIf 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.
```typescript
z.custom().openapi({ type: 'string' });
```## Examples
See the library in use in the [examples](./examples/) folder.
- Simple - [setup](./examples/simple/createSchema.ts) | [openapi.yml](./examples/simple/openapi.yml) | [redoc documentation](https://samchungy.github.io/zod-openapi/examples/simple/redoc-static.html)
## Ecosystem
- [fastify-zod-openapi](https://github.com/samchungy/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](https://github.com/samchungy/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.
## Comparisons
### [@asteasolutions/zod-to-openapi](./docs/comparisons.md)
## Development
### Prerequisites
- Node.js LTS
- pnpm```shell
pnpm
pnpm build
```### Test
```shell
pnpm test
```### Lint
```shell
# Fix issues
pnpm format# Check for issues
pnpm lint
```### Release
To release a new version
1. Create a [new GitHub Release](https://github.com/samchungy/zod-openapi/releases/new)
2. Select `🏷️ Choose a tag`, enter a version number. eg. `v1.2.0` and click `+ Create new tag: vX.X.X on publish`.
3. Click the `Generate release notes` button and adjust the description.
4. Tick the `Set as the latest release` box and click `Publish release`. This will trigger the `Release` workflow.
5. Check the `Pull Requests` tab for a PR labelled `Release vX.X.X`.
6. Click `Merge Pull Request` on that Pull Request to update master with the new package version.To release a new beta version
1. Create a [new GitHub Release](https://github.com/samchungy/zod-openapi/releases/new)
2. Select `🏷️ 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`.
3. Click the `Generate release notes` button and adjust the description.
4. Tick the `Set as a pre-release` box and click `Publish release`. This will trigger the `Prerelease` workflow.