Zod 到 OpenAPI
[大 对于Zod v3支持,请使用v7.3.4版本. 然而,我们并不打算积极支持这一版本 安装方式 :
npm install @asteasolutions/[email protected]
一个使用 zod 计划 生成 Open API Swagger 文档.
我们保留一个更衣日志 作为的一部分 GitHub 发布。 。 。 .
目的和简单的例子
我们到了 Astea 解决方案 因为我们使用 佐德语 用于在我们的 API 中验证, 并厌倦了重复, 也支持一个必须同步的单独的 OpenAPI 定义 。 使用 zod-to-openapi,我们直接从我们的zod schemas生成OpenAPI定义,从而拥有单一的真理来源.
简而言之,它会变成这样:
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,
},
},
},
},
});
改为:
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'
还可以使用 UserSchema 编辑 request.params 对象以校验您的 API 输入.
使用量
安装
npm install @asteasolutions/zod-to-openapi
# or
yarn add @asteasolutions/zod-to-openapi
这个 openapi 方法
为了保持Openapi定义的自然性,我们增加了一个 openapi 用于所有 Zod 对象的方法。 它的想法是提供一个方便的方法来提供OpenApi特定的数据.
它有三个超载:
.openapi({ [key]: value })- 这样我们可以指定任何 OpenApi 字段 。 举个例子z.number().openapi({ example: 3 })将添加example: 3{\fn黑体\fs22\bord1\shad0\3ahbe\4ah00\fscx67\fscy66\2chffffff\3ch808080}到生成的图案..openapi("<schema-name>")- 这样,我们规定基本的zod计划应该“注册”,即加入components/schemas提供<schema-name>.openapi("<schema-name>", { [key]: value })- 将上面的两种情况结合起来,以便我们可同时说明登记情况<schema-name>和其他元数据
为了成功,你需要打电话 extendZodWithOpenApi 一次在你的项目.
这只应在您的项目的共入点文件中进行一次(例如: index.ts编号 :app.ts) (中文(简体) ). 如果你用Webpack来刮树 把文件标记为有副作用.
在您的密码库中实现这一点可能有点棘手,因为 需求 是同步和 导入 是一个aync.
使用 Zod 的 .meta 软件
从v8(和zod v4)开始,您也可以使用zod的.meta来提供元数据,我们将相应读取.
有了Zod生成 JSON 计划和维护注册的新选项 我们为所有元数据信息添加了相当无缝的支持 .meta 调用,好像元数据输入 .openapi。 。 。 .
因此,以下两个计划产生完全相同的结果:
const schema = z
.string()
.openapi('Schema', { description: 'Name of the user', example: 'Test' });
const schema2 = z
.string()
.meta({ id: 'Schema2', description: 'Name of the user', example: 'Test' });
说明: 这也意味着,除非你使用一些我们更复杂的情景, 你甚至可以生成一个计划 不使用
extendZodWithOpenApi在您的密码库中只依赖.meta以提供额外的元数据信息和计划名称(使用id(b) 财产).
需要使用的情况 extendZodWithOpenApi 财务报告和审定财务报表 .openapi
- 当扩展既注册又希望扩展的系统
anyOf即:
const schema = z.object({ name: z.string() }).openapi('Schema');
const schema2 = schema.extend({ age: z.number() }).openapi('Schema2'); // this one would have anyOf and a reference to the first one
- 定义参数元数据. 例如,在做的时候:
registry.registerPath({
// ...
request: {
query: z.object({
name: z.string().openapi({
description: 'Schema level description',
param: { description: 'Param level description' },
}),
}),
},
});
其结果是:
"parameters": [
{
"schema": {
"type": "string",
"description": "Schema level description" // comes directly from description
},
"required": true,
"description": "Param level description", // comes from param.description
"name": "name",
"in": "query"
}
],
基本想法
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' });
例1: 使用 tsx 调用 openapi 扩展
//zod-extend.ts
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';
extendZodWithOpenApi(z);
// package.json
"scripts": {
"start": "tsx --import ./zod-extend.ts ./index.ts",
例2 - 需要语法
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';
extendZodWithOpenApi(z);
const { startServer } = require('./server/start');
startServer();
书记官处
这个 OpenAPIRegistry 用于收集定义的有用工具,这些定义随后将转给 OpenApiGeneratorV3 或者说 OpenApiGeneratorV31 举个例子.
import {
OpenAPIRegistry,
OpenApiGeneratorV3,
} from '@asteasolutions/zod-to-openapi';
const registry = new OpenAPIRegistry();
// Register definitions here
const generator = new OpenApiGeneratorV3(registry.definitions);
return generator.generateComponents();
发电机
有两个发电机可以使用 - OpenApiGeneratorV3 财务报告和审定财务报表 OpenApiGeneratorV31。它们共享相同的界面,但内部生成计划,以正确遵循特定开放 API 版本的数据格式 - 3.0.x 或者说 3.1.x。开放 API 版本会影响某些组件的生成方式.
例如: 将生成器从 OpenApiGeneratorV3 改为 OpenApiGeneratorV31 将导致以下差异:
z.string().nullable().openapi({refId: 'name'});
# 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
两种生成器在其构造器中都采用单一的参数——一系列定义——即来自登记册或普通的zod计划.
两台发电机的公用方法如下:
generateComponents 将只生成 /components openAPI 文档的章节(例如 schemas 财务报告和审定财务报表 parameters),没有生成实际路线.
generateDocument 将生成整个 OpenAPI 文档.
定义计划
OpenApi 计划应通过使用 .openapi 方法和提供名称:
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]);
通过使用 register 方法 OpenAPIRegistry 举个例子 要多检查一下 "使用计划与注册" 节
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);
如果现在跑, generator.generateComponents() 将生成以下结构:
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
输出中计划的关键是第一个传递给 .openapi 方法(或 .register- 在这种情况下: User。 。 。 .
请注意: generateComponents 不返回 YAML 而是 JS 对象 - 您可以根据您的使用大小写将对象序列化为 YAML 或 JSON .
然后可以通过使用 $ref: #/components/schemas/User 在现有的 OpenAPI JSON 中。 对于通过登记处定义的路由,将自动做到这一点.
默认注意 Zod 对象将导致 "additionalProperties": true 除非使用 strict 或者说 catchall,这与Zod对象的正常使用形成对比 zod.parse 已使用.
定义路由( W)
正在注册路径或网路
使用 OpenAPI 路径注册 registerPath 方法 OpenAPIRegistry 举个例子 使用 : registerWebhook 方法并采用与 registerPath。 。 。 .
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',
},
},
});
以上计划中的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
库中用于 registerPath 为: method, (中文(简体) ), path, (中文(简体) ), request 财务报告和审定财务报表 responses。其他一切直接附在路径定义上.
method- 一个get, (中文(简体) ),post, (中文(简体) ),put, (中文(简体) ),delete财务报告和审定财务报表patch· ;path- 弦 -- -- 是终点的路径;request- 可选对象body, (中文(简体) ),params, (中文(简体) ),query财务报告和审定财务报表headers键,query, (中文(简体) ),params- 作为ZodObjectbody- 带有description(单位:千美元)content记录其中:- 键为 a
mediaType字符串喜欢application/json - 对象为
schema编号zod类型
- 键为 a
headers- 实例ZodObject或任意数组zod实例
responses- 键是状态代码或default对象为description(单位:千美元)content记录其中:- 键为 a
mediaType字符串喜欢application/json - 对象为
schema编号zod类型
- 键为 a
定义路由参数
如果您不想插入所有参数定义, 您可以用 registerParameter 然后引用他们:
const UserIdParam = registry.registerParameter(
'UserId',
z.string().openapi({
param: {
name: 'id',
in: 'path',
},
example: '1212121',
})
);
registry.registerPath({
...
request: {
params: z.object({
id: UserIdParam
}),
},
responses: ...
});
YAML 相当于:
components:
parameters:
UserId:
in: path
name: id
schema:
type: string
example: '1212121'
required: true
'/users/{id}':
get:
...
parameters:
- $ref: '#/components/parameters/UserId'
responses: ...
注意:为了定义适用于参数本身的属性,请使用 param 财产 .openapi。任何在 param 将适用于此参数的计划.
生成完整文档
可用 : generateDocument 方法 OpenApiGeneratorV3 或者说 OpenApiGeneratorV31 举个例子 需要一个参数 - 文件配置 。 也许看起来是这样的:
return generator.generateDocument({
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'My API',
description: 'This is the API',
},
servers: [{ url: 'v1' }],
});
定义自定义组件
您可以定义非 OpenAPI 计划的内容, 包括安全方案、 响应头等 。 见 此测试文件 用于实例.
一个完整的例子
可找到完整的示例代码 这里和YAML对其结果的表述 -- 这里
使用计划对注册
Schemas在引用时会自动被注册. 这意味着如果你有这样的计划:
const schema = z.object({ key: z.string().openapi('Test') }).openapi('Object');
将产生以下结构:
components:
schemas:
Test:
type: 'string',
Object:
type: 'object',
properties:
key:
$ref: '#/components/schemas/Test'
required: ['key']
这不需要使用 OpenAPIRegistry 举个例子.
然而,以下代码可以实现同样的输出:
const registry = new OpenAPIRegistry();
const schema = registry.register(
'Object',
z.object({ key: z.string().openapi('Test') })
);
二、《公约》的主要利益 .registry 方法就是,您可以将登记册作为“收集”工具,将所有这些计划放入其中.
与 .openapi数字 :
// file1.ts
export const Schema1 = ...
// file2.ts
export const Schema2 = ...
new OpenApiGeneratorV3([Schema1, Schema2])
添加一个 NewSchema 输入 file3.ts 需要你手动将这个计划传递到生成器的阵列中.
注:如果 NewSchema 以任何其他计划或路径/网路定义作为参考,该定义仍将出现在所产生的文件中.
与 registry.register数字 :
// 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)
添加一个 NewSchema 输入 file3.ts 并使用 registry.register 不需要您修改生成器构造器 .
结论
使用一个 OpenAPIRegistry 如果您想要您生成的文档包含未引用的策略,则实例将非常有用.
这有时是有用的,例如,当你正在慢慢地将已有的文档与 @asteasolutions/zod-to-openapi 你们一次移出一小块 然后可以直接取自现有文件.
把它加进你的建筑
在项目中的文件中,您可以拥有这样的文件 :
export const registry = new OpenAPIRegistry();
export function generateOpenAPI() {
const config = {...}; // your config comes here
return new OpenApiGeneratorV3(registry.definitions).generateDocument(config);
}
然后使用导出 registry 对象将酌情登记所有计划、参数和路线.
然后可以创建一个执行导出的脚本 generateOpenAPI 函数。 此脚本可以作为您构建步骤的一部分执行, 以便它能够将结果写入一些文件, 如 openapi-docs.json。 。 。 .
生成选项
在某些情况下可以改变Schema生成. 可通过下列两种方式做到这一点:
通过一个全局配置作为生成器的第二个参数 :
const generator = new OpenApiGeneratorV3(registry.definitions, options);
目前支持的全球选项有:
const options = {
unionPreferredType: 'oneOf' | 'anyOf' // configures whether oneOf or anyOf is used when generating a schema for a zod union
sortComponents?: 'alphabetically'; // if sortComponents is passed with the value 'alphabetically' it would sort all schemas and parameters.
// If not - they would appear in the order they were defined
}
一次性使用单一方案通过选项:
// Note it is valid for metadata to be undefined in both of the below cases:
schema.openapi('Schema', metadata, options); // when registering a schema or
schema.openapi(metadata, options) // when simply adding some metadata to it
目前支持的一次性选项有:
const options = {
unionPreferredType: 'oneOf' | 'anyOf' // configures whether oneOf or anyOf is used when generating a schema for a zod union
}
Zod 计划类型
支持类型
目前所有支持类型列表为:
-
ZodAny -
ZodArray -
ZodBigInt -
ZodBoolean -
ZodDate -
ZodDefault -
ZodPrefault -
ZodDiscriminatedUnion- 包括
discriminator联盟中所有 Zod 对象注册时的映射.register()或载有refId。 。 。 .
- 包括
-
ZodEffects -
ZodEnum -
ZodIntersection -
ZodLiteral -
ZodNativeEnum -
ZodNullable -
ZodNumber- 包括
z.number().int()推定为type: 'integer'
- 包括
-
ZodObject- 包括
.catchall产生相应的additionalProperties计划 - 还包括:
strict产生相应的additionalProperties计划
- 包括
-
ZodOptional -
ZodPipeline -
ZodReadonly -
ZodRecord -
ZodString-
添加
format用于:.emoji().cuid().cuid2().ulid().ip().date().datetime().uuid().email().url()
-
添加
pattern页:1.regex()也获得支持${'emoji'} ${z. string (. emoji ()}} ${emoji'} ${emoji'} ${ emoji'} ${'cuid'}} ${z. string (.cuid ()} ${'cuid'} ${'cuid'} ${"cuid"} ${'cuid2'}} ${z. string (.cuid2()}} ${'cuid2'} ${'cuid2'} ${'ulid'} ${z. string (. ulid ()}} ${'ulid'} ${"ulid"} ${'ip'} ${z. string (. ip ()} ${'ip'}
-
-
ZodTuple -
ZodUnion -
ZodUnknown
扩展实例 ZodObject 并导致 OpenApi 定义 allOf
不支持的类型
如果您试图从一个不是上述类型之一的zod schema创建 OpenAPI 计划,那么您将收到一个 UnknownZodTypeError。 。 。 .
您仍可自行注册此类计划,提供 type 通过 .openapi 方法。 如果你觉得理想的行为可以自动实现 毫不犹豫地通过Github Issues 来描述你的案子.
已知问题
z.nullable(schema)不为基本注册计划生成一美元ref。 。 。 .
- 这是一个执行方面的限制.
- 不过,你可以简单地使用
schema.nullable()效果完全一样zod明智,但我们也完全支持.