API Schema Diff 上线前评审:oasdiff / graphql-inspector 实战
上线前如何做 API schema diff 评审:自动化 diff 工具能抓到什么、漏掉什么,以及 OpenAPI 和 GraphQL 仍然需要人工检查的那些点。
现场笔记:schema diff 只是警报
diff 会告诉我字段变了,但不会告诉我哪个客户端把字段当成必填,哪个生成 SDK 把 enum 当成封闭集合,哪个报表依赖旧的 null 行为。diff 亮起来之后,评审才真正开始。
Diff 评审行: 变更:新增 enum 值 refund_status=pending_review Schema 分类:新增 调用方风险:移动端生成客户端用了 exhaustive switch 必须证据:移动端契约测试覆盖未知 enum 发布说明:指定日期前需要客户端动作
那次我差点放行的 diff
几个版本之前,我眼睁睁看着一行 schema 变更让三个移动端客户端挂了四十分钟。字段是 order.total,之前一直是整数。后端做了一次重构,把它改成字符串,这样就能返回精确的小数金额。CI 流水线跑了 oasdiff,类型变更被标了出来,评审人勾上了 breaking-change 标签,然后一句"客户端会处理的"就批了。结果他们没处理。iOS 用 Int.init 解析,拿到的是 nil,购物车页面显示为零。整个评审过程里,没有人真的去看一个真实的客户端调用点。
我现在还是会在每次发布都跑 schema diff,只是它不再是我最信任的那一环。
oasdiff 和 graphql-inspector 真正能抓到什么
自动化 diff 对结构性变更相当擅长,我希望它在每个 PR 上都跑。实际用下来,oasdiff 跑 OpenAPI、graphql-inspector 跑 SDL,可以稳定告诉我这些事:
- 新增和删除的 endpoint、operation、query、mutation。
- 字段类型变更:integer 变 string、object 变 array、scalar 变 nullable 包装。
- 请求和响应两侧的 required 变 optional、optional 变 required。
- 新增或删除的参数、header、response code。
- 字段重命名、定义上 deprecated 标记的添加或移除。
这些"容易犯的错"它都能兜住。有人不小心删了一个 endpoint,diff 会吼;悄悄加了一个必填 query 参数,diff 也会吼。挺好,留着。
Diff 工具悄悄漏掉的东西
让我栽过跟头的是另一类情况:schema 看起来一模一样,底下的行为却已经翻面了。Diff 工具是个语法级的仪器,下面这些它一个也看不到:
- 同一形状下的语义变更。
user.status以前代表"账单系统里账号是活跃的",现在代表"session store 里账号是活跃的"。同一个字符串字段、同一组枚举值,真相完全不同。 - 错误码含义的变化。
409 Conflict以前只表示"资源重复",重构之后乐观锁冲突也返回 409。状态码一模一样,但客户端该用的重试策略完全不是一回事。 - 鉴权 scope 的变更。 这个 endpoint 现在需要
orders:write而不是orders:read。OpenAPI 的security块或许更新了,但如果 scope 列表维护在一份 policy 文件里,schema diff 什么都看不到。 - 枚举值的新增。 往 order-status 枚举里加一个
REFUNDED看上去是纯增量。但对任何用穷尽式switch的 TypeScript 客户端来说,这就是 breaking;对任何基于代码生成、使用 sealed type 的 Kotlin 或 Swift 客户端也是一样。 - 理论上不 breaking 的 nullability 变更。 响应字段从 required 改成 optional,大多数工具都标为非 breaking。但凡是盲目解引用的客户端都会崩。我在评审里会把这种情况当作 breaking 来处理,不管工具怎么说。
每个 diff 都能归到三类里
我要求评审人把每个变更归到三堆之一。工具给出第一遍分类,最终判断永远由人来做。
- Breaking。 删除、请求侧新增必填字段、响应类型收窄、枚举项删除、鉴权 scope 扩大。
- Non-breaking。 响应结构的纯新增、带安全默认值的可选请求字段、新 endpoint。
- 看着危险其实没事。 改描述措辞、重排 example 的格式、调整 spec 文件里字段的顺序。Diff 吵得很响,但 wire format 根本没动。
第三类的意义是防止评审人麻木。如果每个 diff 都吓人,那就等于没有哪个 diff 真吓人。把无害的那些显式标出来,真正危险的才会被认真对待。
我的评审清单,按顺序来
每次审 schema 变更的 PR 之前,我都会对着同样的五个问题走一遍:
- Diff 里的每一处变动是不是故意的?有没有哪一项其实是某次重构的副产品,没人打算发布?
- 形状相同的地方,行为是不是也相同?挑一个语义可能漂移的 endpoint 抽查一下。
- 有没有一条 changelog,消费方不用读 PR 也能看懂?
- 凡是被标为 breaking 的,弃用窗口是否履行了?有没有迁移说明?
- 新契约是对应一条文档化的验收标准,还是在实现过程中临时发明出来的?
用 Given / When / Then 写验收标准
每次新增或修改一个契约,我会在动 schema 之前先把期望行为写成一个场景。这种做法逼我去注意到 diff 抓不到的语义漂移。
- Given a client on v2.3 of the orders API
When it requests GET /orders/{id} for a refunded order
Then the response returns 200 with status="REFUNDED"
And clients written before REFUNDED existed receive an enum value
they do not recognize and must fall back to "UNKNOWN"
- Given an integration using the 409 response to trigger a duplicate-submit dialog
When optimistic lock contention also returns 409
Then the release notes flag the overloaded meaning
And a new error code is introduced instead of reusing 409
评审结束之后真正留下来的是这个场景。Diff 只是提醒我"场景该写了"的闹钟。
我实际在用的 CI 门禁
流水线里就两条规则,不多:
- 每个 PR 针对上一版发布的 spec 跑一次
oasdiff breaking(或 GraphQL 对应工具)。只要它发现任何问题,这个 PR 必须带上由 code owner 批准的breaking-change标签才能合并。 - 只要这个标签在,就必须关联一条 changelog。没打标签就不强制 changelog;打了标签而 changelog 是空的,merge 直接被挡。
有一件事我故意不做:自动批准非 breaking 的变更。工具还没聪明到能替我下这个判断。评审人依旧会看每一份 diff,因为 nullability 和枚举这两个陷阱恰好就住在"非 breaking"那一格里。
和版本策略怎么接上
Schema diff 评审是你所选版本策略的证据来源。跑 semver?"breaking"那一堆决定 major 还是 minor。跑基于日期的版本加弃用窗口?diff 告诉你弃用窗口什么时候开始计时。跑单一 evergreen 版本?diff 就是你每周向自己证明"确实向后兼容"的那份依据。还有一条很实在的规则:能避免就不要在同一个 release 里同时发 breaking 和非 breaking 的变更。把 breaking 变更单独放出去,沟通更清晰,回滚也更容易。
那次 integer 变 string 的事故之后我改了什么
Diff 工具本身没问题,是人没审到位。我后来改了自己的流程:
- 对已有字段做任何类型变更,现在必须附上至少一个 SDK 的真实客户端调用样例,证明解析路径还能走通。
- 响应字段的 required 改 optional,一律按 breaking 评审,不管工具怎么说。
- 新增枚举值之前,先在 first-party 客户端里用 grep 找一遍穷尽式
switch,再考虑合并。 - Changelog 专门开一节"semantic changes",记录那些"形状没变但含义变了"的情况。
这些做法一点都不花哨,但任何一条放在当时,都能在 order.total 那次变更到达手机之前把它拦下来。
评审时看什么
这篇文章适合用在API schema diff 上线前评审时。别从“原则”聊起,直接拿一条真实改动来对照,看看规格里还缺什么。
- 把删除字段、重命名、枚举收窄和必填变化单独列出。
- 确认真实调用方是否依赖被改动字段。
- 为兼容变更写清版本、公告和测试窗口。
- 不要让 diff 工具替代人判断字段语义。
diff 报告只是入口。最后要写进发布说明的是:哪些变更允许上线,哪些必须延期或升版本。
Schema diff 后面还要有人读语义
自动 diff 能告诉你字段删了没有、类型窄了没有,却看不出“含义悄悄变了”。我会让 reviewer 在 diff 结果下面补一段人工判断,尤其是默认值、枚举、分页和错误语义。
Human semantic review: - Field added: customer.tier, nullable, default absent - Risk: mobile client may sort unknown tier after paid tiers - Fixture added: old client receives tier=null and renders "standard" - Error shape: unchanged - Pagination: cursor remains opaque, no ordering promise added - Decision: safe additive release, monitor client parse errors for 48h
边界:不要把 schema diff 当成发布批准。它只是第一关。涉及语义、计费、权限或排序规则的变化,即使 diff 显示“非破坏”,也要人读。
我会额外检查缓存和分页
schema diff 很少提醒你缓存 key、分页 cursor 和默认排序。只要 API 返回列表,就要写明新增字段是否进入缓存,状态变化是否影响排序,旧 cursor 在发布后是否还能继续翻页。测试证据最好包含一组发布前生成的 cursor。
可复制产物:契约评审包
当工作涉及 API 行为、schema、事件、重试或消费者预期时使用。它会把兼容性和发布证据提前摊开。
API 契约评审包:API Schema Diff 上线前评审:oasdiff / graphql-inspector 实战 本次要做的决策: - 确认契约变化是否兼容,消费者需要什么迁移动作,发布后如何观察风险。 责任人检查: - 产品责任人: - 工程责任人: - QA 或运维评审: 范围边界: - 本次包含: - 本次不包含: - 仍需确认的假设: 验收证据: - 测试或 fixture: - 日志、指标或截图: - 人工复核步骤: 契约边界:没有兼容性分类、消费者影响、重试行为和回滚说明,不进入发布。 评审追问: - 没参加需求会的人还会误解哪里? - 哪个证据能证明这次改动足够安全,可以发布?
旗舰使用路径
这是 Spec Coding 用来承接「API Schema 发布评审」主题的核心参考页之一。建议把它放到真实工单、PR 或发布评审里使用,而不是只当背景文章阅读。
- 适合从这里开始:OpenAPI 或 GraphQL schema 发生变化,旧客户端可能依赖原行为。
- 建议复制:schema diff 分类表。
- 需要附上的证据:调用方列表、兼容性判断、契约测试结果和发布说明。
- 搭配使用:API 契约 Hub 与 API 规格生成器。
旗舰页使用路径: - 在计划或评审时打开本文。 - 把对应产物复制到工单或 PR。 - 用自己的系统、责任人和失败模式替换示例值。 - 如果证据行仍为空,就不要进入实现。
二次审阅记录:分类行为,而不只是形状
这次检查的重点是避免文章像泛泛 API 清单。新增评审语言更聚焦调用方影响和发布证据。
Schema 评审决策: - 安全:旧调用方会忽略,或已有测试证明可容忍。 - 有风险:旧调用方可能解析、排序、缓存或 switch 它。 - 破坏性:旧调用方不改代码会失败或行为错误。 - 未知:调用方列表或代表性测试缺失。
编辑复核记录
复核日期:2026-04-29。本次补充了可复用产物,按相关主题 Hub 检查了文章定位,并收紧下一步链接,让页面更像可操作参考,而不是孤立长文。
专题阅读路径
这篇文章归入 API 契约 主题。先读 Hub,再结合下面的清单、模板或工具落到具体项目里。
继续阅读
编辑说明与免责声明
最近复核:2026-04-29。编辑部检查了示例、内链和可复制评审片段,确保内容更适合真实项目使用。
本文用于软件工程教学与实践参考,不构成法律、税务或投资建议。示例场景用于解释规格方法,不对应真实客户数据。
- 作者信息:Spec Coding 编辑部
- 编辑政策:编辑与事实核查政策
- 联系方式:联系页面