@asteasolutions/zod-to-openapi
Advanced tools
Comparing version 1.0.0 to 1.0.1
@@ -10,1 +10,2 @@ export declare function isUndefined<T>(value: any): value is undefined; | ||
}>>(object: T, predicate: (val: T[keyof T]) => boolean): Result; | ||
export declare function compact<T extends any>(arr: (T | null | undefined)[]): T[]; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.omitBy = exports.omit = exports.mapValues = exports.isNil = exports.isUndefined = void 0; | ||
exports.compact = exports.omitBy = exports.omit = exports.mapValues = exports.isNil = exports.isUndefined = void 0; | ||
function isUndefined(value) { | ||
@@ -40,1 +40,5 @@ return value === undefined; | ||
exports.omitBy = omitBy; | ||
function compact(arr) { | ||
return arr.filter((elem) => !isNil(elem)); | ||
} | ||
exports.compact = compact; |
@@ -22,2 +22,3 @@ import { OpenAPIObject, InfoObject, ServerObject, SecurityRequirementObject, TagObject, ExternalDocumentationObject, ComponentsObject } from 'openapi3-ts'; | ||
private generateParameterDefinition; | ||
private getParameterRef; | ||
private generateInlineParameters; | ||
@@ -24,0 +25,0 @@ private generateParameter; |
@@ -18,2 +18,3 @@ "use strict"; | ||
const zod_2 = require("zod"); | ||
const errors_1 = require("./errors"); | ||
class OpenAPIGenerator { | ||
@@ -65,3 +66,3 @@ constructor(definitions) { | ||
} | ||
throw new Error('Invalid definition type'); | ||
throw new errors_1.ZodToOpenAPIError('Invalid definition type'); | ||
} | ||
@@ -76,17 +77,42 @@ generateParameterDefinition(zodSchema) { | ||
} | ||
getParameterRef(schemaMetadata, external) { | ||
const parameterMetadata = schemaMetadata === null || schemaMetadata === void 0 ? void 0 : schemaMetadata.param; | ||
const existingRef = (schemaMetadata === null || schemaMetadata === void 0 ? void 0 : schemaMetadata.refId) | ||
? this.paramRefs[schemaMetadata.refId] | ||
: undefined; | ||
if (!(schemaMetadata === null || schemaMetadata === void 0 ? void 0 : schemaMetadata.refId) || !existingRef) { | ||
return undefined; | ||
} | ||
if ((parameterMetadata && existingRef.in !== parameterMetadata.in) || | ||
((external === null || external === void 0 ? void 0 : external.in) && existingRef.in !== external.in)) { | ||
throw new errors_1.ConflictError(`Conflicting location for parameter ${existingRef.name}`, { | ||
key: 'in', | ||
values: (0, lodash_1.compact)([ | ||
existingRef.in, | ||
external === null || external === void 0 ? void 0 : external.in, | ||
parameterMetadata === null || parameterMetadata === void 0 ? void 0 : parameterMetadata.in, | ||
]), | ||
}); | ||
} | ||
if ((parameterMetadata && existingRef.name !== parameterMetadata.name) || | ||
((external === null || external === void 0 ? void 0 : external.name) && existingRef.name !== (external === null || external === void 0 ? void 0 : external.name))) { | ||
throw new errors_1.ConflictError(`Conflicting names for parameter`, { | ||
key: 'name', | ||
values: (0, lodash_1.compact)([ | ||
existingRef.name, | ||
external === null || external === void 0 ? void 0 : external.name, | ||
parameterMetadata === null || parameterMetadata === void 0 ? void 0 : parameterMetadata.name, | ||
]), | ||
}); | ||
} | ||
return { | ||
$ref: `#/components/parameters/${schemaMetadata.refId}`, | ||
}; | ||
} | ||
generateInlineParameters(zodSchema, location) { | ||
const metadata = this.getMetadata(zodSchema); | ||
const parameterMetadata = metadata === null || metadata === void 0 ? void 0 : metadata.param; | ||
const existingRef = (metadata === null || metadata === void 0 ? void 0 : metadata.refId) | ||
? this.paramRefs[metadata.refId] | ||
: undefined; | ||
if ((metadata === null || metadata === void 0 ? void 0 : metadata.refId) && existingRef) { | ||
if (existingRef.in !== location) { | ||
throw new Error(`The parameter ${existingRef.name} was created with \`in: ${existingRef.in}\` but was used as ${location} parameter`); | ||
} | ||
return [ | ||
{ | ||
$ref: `#/components/parameters/${metadata.refId}`, | ||
}, | ||
]; | ||
const referencedSchema = this.getParameterRef(metadata, { in: location }); | ||
if (referencedSchema) { | ||
return [referencedSchema]; | ||
} | ||
@@ -96,11 +122,25 @@ if (zodSchema instanceof zod_1.ZodObject) { | ||
const parameters = Object.entries(propTypes).map(([key, schema]) => { | ||
var _a; | ||
const innerMetadata = this.getMetadata(schema); | ||
const referencedSchema = this.getParameterRef(innerMetadata, { | ||
in: location, | ||
name: key, | ||
}); | ||
if (referencedSchema) { | ||
return referencedSchema; | ||
} | ||
const innerParameterMetadata = innerMetadata === null || innerMetadata === void 0 ? void 0 : innerMetadata.param; | ||
if ((innerParameterMetadata === null || innerParameterMetadata === void 0 ? void 0 : innerParameterMetadata.name) && | ||
innerParameterMetadata.name !== key) { | ||
throw new Error(`Conflicting name - a parameter was created with the key "${key}" in ${location} but has a name "${innerParameterMetadata.name}" defined with \`.openapi()\`. Please use only one.`); | ||
throw new errors_1.ConflictError(`Conflicting names for parameter`, { | ||
key: 'name', | ||
values: [key, innerParameterMetadata.name], | ||
}); | ||
} | ||
if ((innerParameterMetadata === null || innerParameterMetadata === void 0 ? void 0 : innerParameterMetadata.in) && | ||
innerParameterMetadata.in !== location) { | ||
throw new Error(`Conflicting location - the parameter "${innerParameterMetadata.name}" was created within "${location}" but has a in: "${innerParameterMetadata.in}" property defined with \`.openapi()\`. Please use only one.`); | ||
throw new errors_1.ConflictError(`Conflicting location for parameter ${(_a = innerParameterMetadata.name) !== null && _a !== void 0 ? _a : key}`, { | ||
key: 'in', | ||
values: [location, innerParameterMetadata.in], | ||
}); | ||
} | ||
@@ -112,3 +152,6 @@ return this.generateParameter(schema.openapi({ param: { name: key, in: location } })); | ||
if ((parameterMetadata === null || parameterMetadata === void 0 ? void 0 : parameterMetadata.in) && parameterMetadata.in !== location) { | ||
throw new Error(`Conflicting location - the parameter "${parameterMetadata.name}" was created within "${location}" but has a in: "${parameterMetadata.in}" property defined with \`.openapi()\`. Please use only one.`); | ||
throw new errors_1.ConflictError(`Conflicting location for parameter ${parameterMetadata.name}`, { | ||
key: 'in', | ||
values: [location, parameterMetadata.in], | ||
}); | ||
} | ||
@@ -125,7 +168,9 @@ return [ | ||
if (!paramName) { | ||
throw new Error('Missing parameter name, please specify `name` and other OpenAPI props using `ZodSchema.openapi`'); | ||
throw new errors_1.MissingParameterDataError({ missingField: 'name' }); | ||
} | ||
// TODO: Might add custom errors. | ||
if (!paramLocation) { | ||
throw new Error(`Missing parameter location for parameter ${paramName}, please specify \`in\` and other OpenAPI props using \`ZodSchema.openapi\``); | ||
throw new errors_1.MissingParameterDataError({ | ||
missingField: 'in', | ||
paramName, | ||
}); | ||
} | ||
@@ -223,3 +268,3 @@ const required = !zodSchema.isOptional() && !zodSchema.isNullable(); | ||
if (!(metadata === null || metadata === void 0 ? void 0 : metadata.description)) { | ||
throw new Error('Missing response description. Please specify `description` and using `ZodSchema.openapi`.'); | ||
throw new errors_1.MissingResponseDescriptionError(); | ||
} | ||
@@ -233,3 +278,3 @@ return { | ||
if (!(metadata === null || metadata === void 0 ? void 0 : metadata.description)) { | ||
throw new Error('Missing response description. Please specify `description` and using `ZodSchema.openapi`.'); | ||
throw new errors_1.MissingResponseDescriptionError(); | ||
} | ||
@@ -323,5 +368,6 @@ return { | ||
const refId = (_e = this.getMetadata(zodSchema)) === null || _e === void 0 ? void 0 : _e.refId; | ||
const errorFor = refId ? ` for ${refId}` : ''; | ||
throw new Error(`Unknown zod object type${errorFor}, please specify \`type\` and other OpenAPI props using \`ZodSchema.openapi\`. The current schema is: ` + | ||
JSON.stringify(zodSchema._def)); | ||
throw new errors_1.UnknownZodTypeError({ | ||
currentSchema: zodSchema._def, | ||
schemaName: refId, | ||
}); | ||
} | ||
@@ -328,0 +374,0 @@ toOpenAPIObjectSchema(zodSchema, isNullable) { |
{ | ||
"name": "@asteasolutions/zod-to-openapi", | ||
"version": "1.0.0", | ||
"version": "1.0.1", | ||
"description": "Builds OpenAPI schemas from Zod schemas", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
289
README.md
# Zod to OpenAPI | ||
A library that uses zod schemas to generate an Open API Swagger documentation. | ||
A library that uses [zod schemas](https://github.com/colinhacks/zod) to generate an Open API Swagger documentation. | ||
1. [Purpose](#purpose) | ||
1. [Purpose and quick example](#purpose-and-quick-example) | ||
2. [Usage](#usage) | ||
1. [Installation](#installation) | ||
2. [Expanding the zod functionalities](#expanding-the-zod-functionalities) | ||
3. [Generating components](#generating-components) | ||
4. [Registering schema definitions](#registering-schema-definitions) | ||
5. [Registering parameter definitions](#registering-parameter-definitions) | ||
6. [Generating a full OpenAPI document](#generating-a-full-openapi-document) | ||
- [Registering a path](#registering-a-path) | ||
2. [The `openapi` method](#the-openapi-method) | ||
3. [The Registry](#the-registry) | ||
4. [Defining schemas](#defining-schemas) | ||
6. [Defining routes](#defining-routes) | ||
7. [A full example](#a-full-example) | ||
@@ -18,8 +16,80 @@ 8. [Adding it as part of your build](#adding-it-as-part-of-your-build) | ||
<!-- TODO: Something about a CHANGELOG --> | ||
We keep a changelog as part of the [GitHub releases](https://github.com/asteasolutions/zod-to-openapi/releases). | ||
## Purpose | ||
## Purpose and quick example | ||
We at [Astea Solutions](https://asteasolutions.com/) made this library because of the duplication of work when creating a documentation for an API that uses `zod` to validate request input and output. | ||
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, this having single source of truth. | ||
Simply put, it turns this: | ||
```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 }), | ||
}) | ||
); | ||
registry.registerPath({ | ||
method: 'get', | ||
path: '/users/{id}', | ||
summary: 'Get a single user', | ||
request: { | ||
params: z.object({ id: z.string() }), | ||
}, | ||
responses: { | ||
200: { | ||
mediaType: 'application/json', | ||
schema: UserSchema.openapi({ | ||
description: 'Object with user data', | ||
}), | ||
} | ||
}, | ||
}); | ||
``` | ||
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 | ||
@@ -35,8 +105,7 @@ | ||
### Expanding the zod functionalities | ||
### The `openapi` method | ||
In order to specify some OpenAPI specific metadata you should use the exported `extendZodWithOpenApi` | ||
function with your own instance of `zod`. | ||
To keep openapi definitions natural, we add an `openapi` method to all Zod objects. For this to work, you need to call `extendZodWithOpenApi` once in your project. | ||
Note: This should be done only once in a common-entrypoint file of your project (for example an `index.ts`/`app.ts`) | ||
Note: 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. | ||
@@ -53,6 +122,5 @@ ```ts | ||
### Generating components | ||
### The Registry | ||
The `OpenAPIRegistry` class is used as a utility for creating definitions that are then to be used to | ||
generate the OpenAPI document using the `OpenAPIGenerator` class. In order to generate components the `generateComponents` method should be used. | ||
The `OpenAPIRegistry` is used to track definitions which are later generated using the `OpenAPIGenerator` class. | ||
@@ -74,4 +142,6 @@ ```ts | ||
### Registering schema definitions | ||
`generateComponents` will generate only the `/components` section of an OpenAPI document (e.g. only `schemas` and `parameters`), not generating actual routes. | ||
### Defining schemas | ||
An OpenAPI schema should be registered using the `register` method of an `OpenAPIRegistry` instance. | ||
@@ -83,11 +153,5 @@ | ||
z.object({ | ||
id: z.string().openapi({ | ||
example: '1212121', | ||
}), | ||
name: z.string().openapi({ | ||
example: 'John Doe', | ||
}), | ||
age: z.number().openapi({ | ||
example: 42, | ||
}), | ||
id: z.string().openapi({ example: '1212121' }), | ||
name: z.string().openapi({ example: 'John Doe' }), | ||
age: z.number().openapi({ example: 42 }), | ||
}) | ||
@@ -97,83 +161,36 @@ ); | ||
The YAML equivalent of the schema above would be: | ||
If run now, `generateComponents` will generate the following structure: | ||
```yaml | ||
User: | ||
type: object | ||
properties: | ||
id: | ||
type: string | ||
example: '1212121' | ||
name: | ||
type: string | ||
example: John Doe | ||
age: | ||
type: number | ||
example: 42 | ||
required: | ||
- id | ||
- name | ||
- age | ||
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 | ||
``` | ||
Note: All properties defined inside `.openapi` of a single zod schema are applied at their appropriate schema level. | ||
The key for the schema in the output is the first argument passed to `.register` (in this case - `User`). | ||
The result would be an object like `{ components: { schemas: { User: {...} } } }`. The key for the object is the value of the first argument passed to `.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. | ||
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. | ||
### Registering parameter definitions | ||
### Defining routes | ||
An OpenAPI parameter (query/path/header) should be registered using the `registerParameter` method of an `OpenAPIRegistry` instance. | ||
```ts | ||
const UserIdSchema = registry.registerParameter( | ||
'UserId', | ||
z.string().openapi({ | ||
param: { | ||
name: 'id', | ||
in: 'path', | ||
}, | ||
example: '1212121', | ||
}) | ||
); | ||
``` | ||
Note: Parameter properties are more specific to those of an OpenAPI schema. 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. | ||
The YAML equivalent of the schema above would be: | ||
```yaml | ||
UserId: | ||
in: path | ||
name: id | ||
schema: | ||
type: string | ||
example: '1212121' | ||
required: true | ||
``` | ||
The result would be an object like `{ components: { parameters: { UserId: {...} } } }`. The key for the object is the value of the first argument passed to `.registerParameter` (in this case - `UserId`). | ||
The resulting schema can then be referenced by using `$ref: #/components/parameters/UserId` in an existing OpenAPI JSON. | ||
### Generating a full OpenAPI document | ||
A full OpenAPI document can be generated using the `generateDocument` method of an `OpenAPIGenerator` 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' }], | ||
}); | ||
``` | ||
#### Registering a path | ||
An OpenAPI path should be registered using the `registerPath` method of an `OpenAPIRegistry` instance. | ||
An OpenAPI path is registered using the `registerPath` method of an `OpenAPIRegistry` instance. | ||
@@ -187,3 +204,5 @@ ```ts | ||
request: { | ||
params: z.object({ id: UserIdSchema }), | ||
params: z.object({ | ||
id: z.string().openapi({ example: '1212121' }) | ||
}), | ||
}, | ||
@@ -239,2 +258,68 @@ responses: { | ||
#### 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 `OpenAPIGenerator` 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' }], | ||
}); | ||
``` | ||
### A full example | ||
@@ -260,3 +345,3 @@ | ||
Then you can create a script that can execute 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`. | ||
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`. | ||
@@ -267,2 +352,2 @@ ## Technologies | ||
- [Zod 3.x](https://github.com/colinhacks/zod) | ||
- [OpenAPI 3.x](https://github.com/metadevpro/openapi3-ts) | ||
- [OpenAPI 3.x TS](https://github.com/metadevpro/openapi3-ts) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
42981
15
757
344