JSON Schema 最佳实践:draft-07 基础与设计取舍

JSON Schema 描述「一份 JSON 该长什么样」,是 API 契约与数据校验的通用语言。本文先讲清 draft-07 的核心关键字,再谈 required、 enum、 format 的取舍,所有示例都能在 JSON Schema 推断 工具里复现。

一、示例 JSON

下面这份用户样例是全文的推断来源,粘贴进站内工具的输入框即得到后续 Schema:

{
  "id": 1,
  "name": "Ada",
  "email": "ada@example.com",
  "created_at": "2026-01-01T00:00:00Z",
  "active": true,
  "role": "admin",
  "tags": ["a", "b"],
  "address": { "city": "Shanghai", "zip": "200000" }
}

二、draft-07 核心关键字

  • type:节点的类型。站内推断把整数标为 integer、浮点标为 number;两者合并时统一退化为 number。
  • properties:对象的字段定义,每个键对应一个子 Schema,可任意嵌套。示例中 address 会展开成含 city / zip 的子对象。
  • required:字符串数组,列出必须有值的字段。默认把样例里出现的属性全部写入。
  • items:数组元素的 Schema。空数组无法推断元素类型,会输出空 schema {},请至少给一条元素样例。
  • enum:取值白名单,写在 items 上时表示数组每个元素都从该集合取值。
  • format:语义提示,如 date、 date-time、 time、 email、 uuid、 uri。

和 TypeScript interface 的区别:TS 只在编译期约束代码,JSON Schema 是运行期契约,可校验任意来源的 JSON (跨语言、跨进程);TS 无法表达 enum 之外的取值白名单与 format 校验,Schema 可以,并且能反过来生成示例数据。

三、required 的取舍:安全 vs 演进

required 是「契约强度」。全部标记时,任何缺字段的报文都会校验失败,能及早暴露上游问题;代价是接口稍作演进(新增必填字段前的过渡期)就会误报。 一个都不标,规则就形同虚设。推荐做法:

  • 先用推断的默认结果(全标)作为「理想契约」参考;
  • 再按真实接口约定把真正可选、可缺省的字段从 required 移除;
  • 多份样例合并时,站内推断对 required 取交集,只有每条样例都出现的字段才会保留,天然筛掉偶发字段。

四、enum 与 format 的取舍

  • enum 有阈值。站内推断只在数组项为 2–8 个互不相同的原始值、且类型单一时标注 enum。 少于 2 个没区分度,多于 8 个则把契约绑成「闭集」,新增取值都要改 Schema,此时应退回普通 type。
  • format 会「看走眼」。它基于形状匹配,长得像邮箱的字符串就会命中。站内推断在合并多份样例时,只有两侧 format 完全一致才保留;不一致就丢成普通 string,避免把偶发形状固化成约束。
  • 日期会二次校验。仅形状像日期还不够,站内推断会再用真实日历校验(例如 2026-13-40 不会标成 date),减少误报。

五、anyOf / oneOf:结构冲突怎么表达

当同一字段在不同样例里类型不同(一会儿字符串、一会儿对象),无法合并成单一 Schema,站内推断会折叠为 anyOf。设计建议:

  • 「满足其一即可」→ anyOf;「恰好满足其一」→ oneOf(互斥取值,如多态消息)。
  • 作为页面里「响应形态不确定」的表达,anyOf 比硬塞一个模糊的 string 更诚实,但下游代码需分支处理,能用统一结构就别用组合关键字。
  • 数组多样例合并时,同名字段递归合并、required 取交集、类型冲突向上折叠为 anyOf。

六、从 Schema 回到数据

契约写好后要能自证。站内 Schema → JSON 示例 工具按 draft-07 语义生成符合约束的数据:const / enum / default / examples 字面量优先采用,email、date-time 等 format 生成语义化占位值,可选属性可整体开关。这样「推断 → 复核 → 反推数据」就形成了闭环。

七、常见错误

  • 把单份样例的 required 当真理。样例里出现的字段未必必填,务必结合接口约定复核。
  • 空数组期望推断出元素类型。空数组只能给出 items: {},补一条元素样例即可。
  • 用 enum 表达自由文本。取值超过 8 个且会增长时不应使用 enum,否则每加一个取值都要改契约。
  • 把 format 当强校验。多数校验器默认只对 format 做提示而非硬失败,真正强约束还需在业务层补正则或规则。
  • draft 版本混用。站内推断输出 draft-07;若要升级到 2020-12,注意 definitions 改为 $defs 等差异。

动手跑一遍

粘贴本文示例 JSON,停顿即可得到 draft-07 Schema。切换 required / format / enum 开关看差异,再用反推工具生成示例数据。全部在本地完成,数据不上传。

常见问题

单份样例推断出来的 required 可信吗?

结构层面(类型、嵌套、数组项)可信;required 属于归纳,需要复核。站内推断把样例里出现的属性默认全部写入 required,单份样例无法证明某字段「一定存在」。如果接口允许字段缺省,请手工从 required 数组里移除,或直接关闭「标记 required」开关。

enum 什么时候自动识别,什么时候不该用?

站内推断的规则是:数组项全部为原始值(字符串 / 数字 / 布尔 / null)、互不相同且数量在 2–8 个之间、且合并后类型单一,才会在 items 上标注 enum。少于 2 个没有区分度,超过 8 个基本退化为自由取值,这时用 enum 反而把契约绑死,应该改用普通 type 加文档说明。

format 会不会误标?合并时怎么处理?

format 是「形状识别」,例如 [^\s@]+@[^\s@]+\.[^\s@]+ 会被标为 email,但恰好长成邮箱的普通字符串也会命中。站内推断在合并多个样例时,只有当两侧 format 完全一致才保留,否则退化为普通 string,避免把偶发形状固化成约束。

anyOf 和 oneOf 该怎么选?

需要「满足其中之一即可」用 anyOf,多个分支允许同时成立;需要「恰好满足其中一个」用 oneOf,适用于互斥取值(如不同类型的消息)。站内推断在结构不兼容(如某个字段一会儿是字符串、一会儿是对象)时会折叠为 anyOf,因为无法证明分支互斥。

这些结论在站内怎么验证?

打开 JSON Schema 推断工具,粘贴示例后停顿约 400ms 即得到 draft-07 Schema。逐一切换「标记 required」「识别格式」「识别枚举」开关,可以实时对比差异。反向可用「Schema → JSON 示例」把同一份 Schema 还原成数据。

相关工具