OpenAPI 与 JSON Schema:契约怎样被机器校验和生成
一份 OpenAPI 文档把 HTTP 接口组织成可读取的结构:在哪个地址,用什么方法,接收哪些参数和请求体,返回哪些状态与表示。Schema 再描述其中的数据约束。解析器、文档界面、代码生成器和运行时验证器分别使用这些信息,它们执行的工作并不相同。
从 HTTP 操作读懂 OpenAPI
文档的主要组成
OpenAPI 文档
├─ openapi 规范版本,如 3.0.3 或 3.1.1
├─ info.version 这份接口说明的版本
├─ servers 地址及可替换变量
├─ paths
│ └─ /orders/{id}
│ └─ get / put operationId、参数、请求体、响应与安全要求
├─ components 可复用 Schema、参数、响应和认证方案
└─ security 全局安全要求,可被操作级声明覆盖openapi 与 info.version 分别属于规范和业务接口,升级其中一个不会自动改变另一个。operationId 在文档中标识操作,许多生成器用它命名 SDK 方法;路径和网络行为完全没变时,重命名它也可能让客户端代码无法编译。文档结构以 OpenAPI 3.0.3为例。
一个读取操作可以写成:
paths:
/orders/{id}:
get:
operationId: getOrder
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: 当前订单
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'404':
description: 当前身份无法取得该订单这是操作片段,完整文件还需规范版本、info 和被引用的 Order。路径参数必须标为必需。query、header 和 cookie 参数各有独立位置;JSON 请求体应写进 requestBody.content,不能当成一个叫 body 的普通参数。
参数怎样变成线上的字节
数组过滤可能编码为 status=paid&status=shipped,也可能是 status=paid,shipped。OpenAPI 用 style、explode 等字段描述这种序列化选择,默认值与参数位置有关。客户端和网关若采用不同解释,字段类型都正确也会得到不同查询。
| 输入 | 描述时要明确的内容 |
|---|---|
| 路径 ID | 字符串身份、编码方式、合法范围和资源可见性 |
| query 数组 | 重复参数或分隔形式、空数组、顺序是否有意义 |
| JSON 对象 | 媒体类型、结构约束、未知字段及缺失值处理 |
| multipart | 各部分名称、类型、文件与普通字段的编码 |
| 二进制内容 | 媒体类型、长度约束和流式读取条件 |
响应按状态分别描述,还可以列出 ETag、Location、Retry-After 等响应头。只写一个 200 示例会漏掉客户端真正需要处理的创建、无内容、认证、冲突与限流分支。分页排序和错误类型的业务含义也应就地写清楚,不能只留下字段名。
认证方案先在 components.securitySchemes 定义,操作再声明使用哪些方案。一个 Security Requirement 对象内列出的多个方案需要同时满足;数组里的多个对象表示可选方案。空安全要求和操作级覆盖可能使某个操作允许匿名。描述认证方案不会自动在服务端安装认证过滤器,实际授权仍由实现执行。
先写规范还是从代码生成
先写规范适合多个团队或语言一起确定输入输出,生成接口可以帮助实现保持签名一致。从代码生成规范则更容易随框架模型更新,但注解缺失、泛型、多态和异常处理可能使导出的文档不完整。两种方式都需要确定可修改的源头,避免手工修导出文件后下一次构建又被覆盖。
文档 UI 通常侧重操作展示和试请求;Parser 侧重结构、引用与模型;Schema Validator 检查具体数据;Generator 输出语言模型及请求代码。选择工具时,应拿实际使用的 nullable、组合模型、分页和错误响应跑一次,不凭“支持 OpenAPI”推断全部关键字行为。
Schema 怎样约束数据而不改变数据
规范版本和方言
OpenAPI 3.0 的 Schema Object 与通用 JSON Schema 有差异,例如它使用 nullable。3.1 转向 JSON Schema 2020-12 版本的体系,可使用类型联合、$schema 和默认方言声明;Schema 的方言还可能被显式覆盖。OpenAPI 3.1.1解释了这些关系。
OpenAPI 3.2 又增加了规范能力,但生成器与服务框架需要分别实现支持。OpenAPI 3.2.0可以作为规范查阅入口。实验选择 3.0.3 生成 Java SDK,另用独立的 JSON Schema 2020-12 版本演示数据验证,便于直接观察两种工具各自处理的对象;不能把独立 Schema 中的联合类型直接塞回 3.0 Schema。
必需、可空、缺省与格式
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"note": {"type": ["string", "null"], "maxLength": 100},
"priority": {"type": "integer", "minimum": 0, "default": 0}
},
"required": ["note"],
"additionalProperties": false
}required 检查成员是否存在,type 检查存在时的值。所以上例允许 {"note":null},拒绝 {} 和 {"note":12}。properties 中列出成员不会自动把它设为必填;未知字段也只有在额外约束生效时才被拒绝。JSON Schema 对象约束有对应解释。
default 描述缺省语义,不要求验证器修改实例。代码生成器可能把它写入构造函数,有些验证器提供额外的填值开关,业务层也可以自行补值;应明确谁执行这一步。Schema 注解还包括示例、标题、说明、readOnly 和 writeOnly 等元数据。
format: date-time 的验证强度取决于方言与验证器配置。若应用依赖它拒绝非法日期,就应启用相应 assertion 并用坏值检查,不能只看生成类是时间类型。数值范围、字符串长度、数组元素、正则和枚举是另外的约束;验证全部通过后,库存、权限和跨字段业务规则仍需执行。2020-12 版本的 Validation 规范区分了这些关键字。
组合与引用
allOf 要求每个子 Schema 都成立;anyOf 至少一个成立;oneOf 恰好一个成立。它们是逻辑组合,不要求工具生成面向对象继承。两个 oneOf 分支如果同时接受同一实例,实例会失败;应利用必需字段、常量或判别字段形成可区分结构,而不是仅添加一个展示用名称。
封闭对象尤其容易与组合冲突。外层 additionalProperties: false 只识别同一层声明的属性,不会自动把 allOf 里声明的 id 当作已知成员。在支持的方言中,unevaluatedProperties: false 可以利用组合中已经成功评价的属性。实验会用真正验证器对比这两个结果。
引用可以指向当前文档、相对文件或外部地址。相对 $ref 根据文档位置和适用的 base URI 解析;JSON Schema 的 $id 又能改变引用基准。递归结构可以合法存在,树节点引用自身不应被一律判错。应检查工具是否能解析和生成它,以及大小、深度和引用资源是否可控。Schema 结构与引用给出了 URI 和锚点规则。
构建输入来自不可信来源时,禁止解析器随意访问内网地址或无限下载外部文档。实践中可将审核过的依赖固定为仓库文件或不可变制品,生成前建立完整引用集,并在无任意外网访问的环境运行。文档说明中的 HTML、模板和生成文件名也要按不可信输入处理。
解析规范、验证实例并生成客户端
准备独立工程
下载 HTTP 契约实验,在 Linux amd64 的 Bash 中解压进入 contract-http。需要 Docker Engine、unzip 和当前目录写权限。构建以宿主 UID/GID 运行,Java 编译目标 17;Maven 镜像使用 3.9.12。完整 POM 固定 OpenAPI Generator 7.15.0、Swagger Parser 2.1.27、networknt 1.5.6 和 Jackson 2.18.3,用于重现这一套工具组合。
unzip contract-http-lab.zip
cd contract-http
bash run-docker.sh初次运行会下载插件与依赖,正常结果是 13 个测试通过。主要文件如下:
contracts/orders.yaml OAS 3.0.3 的实际生成输入
contracts/note.schema.json JSON Schema 2020-12 版本的实例约束
src/main/.../OrdersServer 实际 HTTP 服务
src/test/.../SchemaTest 解析与验证器测试
src/test/.../ConsumerTest 生成客户端的网络调用
target/generated-sources/ 构建生成的 Java 源码包里的 OAS 使用固定 /orders/o-1 资源,便于让测试集中观察生成和解析;前面带 {id} 的片段用于说明一般参数设计。生成器使用 java/native 库,即 JDK HttpClient,相关选项见 Java Generator。这些版本是实验组合,升级工具时应重新生成、编译和调用,不直接复用旧的成功结论。
三个不同的检查入口
SchemaTest 先将文档交给真实 Swagger Parser,并开启引用解析:
var options = new ParseOptions();
options.setResolve(true);
var result = new OpenAPIV3Parser().readContents(document, null, options);读取成功时,返回对象中的操作 ID 是 getOrder,诊断列表为空。测试接着把 Order 引用改为不存在的 Absent,确认诊断包含该引用。解析器有时仍返回部分模型,所以调用方应同时检查模型和诊断,不能只判断返回值非 null。接口使用方式见 Swagger Parser。
接着以 networknt 验证具体 JSON:
var schema = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012)
.getSchema(Files.readString(Path.of("contracts/note.schema.json")));
var input = new ObjectMapper().readTree("{\"note\":null}");
var errors = schema.validate(input);这个输入没有错误,执行后也没有被补入 priority。测试还分别检查 missing note 的 required、数字 note 的 type 和额外成员的 additionalProperties 错误类别,避免把任何异常都误判为校验生效。所用版本 API 和方言配置见 networknt 1.5.6。
最后,Maven 的 generate-sources 阶段生成 OrdersApi 和 Order,生成代码与服务、测试一起编译。ConsumerTest 启动真实 HTTP 端口并调用 OrdersApi.getOrder(),检查资源 ID、备注和版本,让生成结果经过一次实际请求与响应。
三个阶段的失败指向不同位置:
| 失败 | 先查什么 | 修复后重跑 |
|---|---|---|
| 找不到 Absent 引用 | $ref、文档基准地址、被引用文件 | Parser 与全部引用诊断 |
| note 类型错误 | 实际 JSON 和 Schema 类型 | 同一坏输入仍失败,合法输入通过 |
| allOf 中 id 被判额外成员 | additionalProperties 所在层级 | 封闭组合的正反实例 |
| 生成源码编译失败 | generator/library、依赖、命名和操作 ID | 清理旧产物后重新生成及编译 |
| 调用返回不满足消费者 | 网络响应、字段映射、错误处理 | 真正的 HTTP 消费者用例 |
观察一个“能反序列化”的坏响应
服务的 broken 模式删掉响应中的 id,JSON 仍然语法正确。固定生成器组合能将它解析成对象,但消费者随后要求 id == "o-1",因此断言失败。Schema 写了 required 并不代表每一种生成客户端都会自动执行完整响应校验。
相反,expanded 模式只增加 displayName;当前客户端能够忽略这个未知字段并保持原逻辑。这里测到的是当前 Java 生成器配置,其他语言、严格反序列化器或封闭 Schema 可能拒绝它。支持哪些客户端,需要按实际组合保留用例,不能由这一个结果推出所有消费者兼容。
运行时验证放在哪里也会影响成本。输入可先在入口执行大小限制和结构检查,再进入业务;响应可在测试、预发或特定流量上校验,避免每次请求重复解析同一 Schema。Schema 通常预编译并复用,实例及错误列表按请求隔离。若入口已经做了结构校验,服务层仍要处理授权、并发版本和数据库约束。
修改规范时怎样处理不兼容和工具差异
先判断输入还是输出
服务端新增必填请求字段,会让旧客户端的请求失败;新增响应字段,则要看旧客户端是否允许未知成员。扩大请求允许的取值通常利于旧请求继续工作,扩大响应枚举却可能超出旧客户端的识别范围。比较 Schema 时需要标明读写方向。
字段改名、单位变化、null 改为空字符串、默认排序变化,都可能影响业务,即使结构工具没有报错。对金额、ID、时间和分页的约定见跨语言数据语义。删除操作、响应状态或请求媒体类型,也应与仍在使用它们的客户端一起评估。
避免生成源码和规范互相漂移
生成代码只在明确目录输出,业务扩展放在独立封装层。修复生成结果时,应调整规范、生成器配置或可追踪模板,再重新生成;不要只手改 generated-sources。仓库保留生成器版本、输入引用、配置和消费者测试,发布 SDK 时保留生成产物,便于定位同版本号下的内容差异。
升级生成器可能改变 nullable 包装、枚举未知值、时间类型、方法名称、包名或 HTTP 默认行为。先在独立分支生成差异,编译已有调用代码,再执行成功、错误和兼容响应;网络重试、超时和 TLS 设置也属于 SDK 的实际行为。版本标识与迁移步骤见SDK 与契约测试。
解析或生成失败后的恢复
引用无法解析时,先用本地固定输入重现,并查看完整诊断;不要通过关闭引用检查“恢复”生成。如果只是远端文件不可达,应恢复已锁定内容,确认摘要后再继续。遇到工具未支持的方言,可升级受支持工具,或在不改变业务含义的前提下调整表达;单独把版本字符串从 3.1 改为 3.0 会留下无效关键字。
当候选规范已经生成不兼容 SDK,应暂停使用候选产物,恢复原规范及生成配置,再重新构建原消费者。接口服务是否也要回退取决于已经上线的字段和数据变化。规范文件回滚只恢复描述,不能撤销服务已经产生的业务副作用。
权威资料与规范地址
OpenAPI 规范版本
- OpenAPI 3.0.3:https://spec.openapis.org/oas/v3.0.3.html
- OpenAPI 3.1.1:https://spec.openapis.org/oas/v3.1.1.html
- OpenAPI 3.2.0:https://spec.openapis.org/oas/v3.2.0.html
JSON Schema 结构与验证语义
- JSON Schema 对象:https://json-schema.org/understanding-json-schema/reference/object
- JSON Schema 注解:https://json-schema.org/understanding-json-schema/reference/annotations
- Validation 方言规范:https://json-schema.org/draft/2020-12/json-schema-validation.html
- Schema 结构与引用:https://json-schema.org/understanding-json-schema/structuring
代码生成与解析验证工具
- Java Generator:https://openapi-generator.tech/docs/generators/java/
- Swagger Parser:https://github.com/swagger-api/swagger-parser
- networknt 1.5.6:https://github.com/networknt/json-schema-validator/blob/1.5.6/README.md
