JSON Schema 推断工具能在几秒钟里把一段样本 JSON 转成 schema,但直接用推断结果做线上校验几乎一定会出问题——required 太严、null 语义不对、enum 漏推或乱推、嵌套层级失控。这篇讲清自动推断”能告诉你什么、告诉不了什么”,以及人工补刀该聚焦在哪里。
推断工具能做的事
给一段样本 JSON:
{
"id": 12345,
"name": "Alice",
"email": "alice@example.com",
"tags": ["vip", "early"],
"address": null
}
工具能自动得出:
- 字段类型——
id是 integer、name和email是 string、tags是 array of string - 字段全集——schema 的
properties列出所有字段 - 数组元素类型——遍历样本里的元素求并集
- 简单格式——日期字符串自动加
format: "date-time"、邮箱加format: "email"
这些都是机械可推断的事实——只要样本足够多,结论就准。
推断工具做不到的事
下面这些必须依赖业务知识:
| 决策 | 为什么不能纯推断 |
|---|---|
required | 样本是观察值,“出现过”≠“必有” |
null 语义 | null 是值还是”代表缺失”,看上下文 |
enum 候选 | 候选有限还是无界,看业务 |
| 数字范围 | 样本里 1-100 ≠ 全集 1-100 |
| 字符串约束 | pattern、长度等都来自业务规则 |
| 嵌套深度 | 何处该拆成 $ref 看复用情况 |
下面逐项讲清楚怎么补刀。
required:从”出现过”到”必有”
默认推断策略:单个样本里所有字段都进 required。
问题:
// 单个样本里 metadata 字段恰好有
{ "id": 1, "name": "x", "metadata": { ... } }
// schema 推断成
{ "required": ["id", "name", "metadata"] }
// 真实数据里 50% 没有 metadata → 校验大量失败
正确做法:
- 多样本求交集——给工具喂 5-10 个真实样本,required 取所有样本都出现的字段
- 业务知识补刀——
idcreated_at必有;notemetadata偏 ad-hoc 不放 required - API 文档对照——服务端代码里看哪些字段是必返的
- 保守为先——schema 偏严会大量误报;schema 偏宽只是少抓一些 bad case,可补救
最佳工作流:
推断 → 人工删掉看着可选的 → 用 schema 跑 1000 条生产数据 →
统计 "required missing" 错误 → 把这些字段从 required 移出
null:是值还是缺失代理
JSON 的 null 在不同语义下应该写不同 schema:
语义 1:null 表示”明确没有这个值”
{ "parent_id": null } // 这是根节点
字段必有,值可以是 null:
{
"type": "object",
"properties": {
"parent_id": { "type": ["string", "null"] }
},
"required": ["parent_id"]
}
语义 2:null 表示”字段未填”
应该让字段缺席,而不是值为 null:
{
"type": "object",
"properties": {
"note": { "type": "string" }
}
// note 不在 required 里
}
工具默认:看到 null 一律推成 ["原类型", "null"]——保守但未必符合业务。
JSON Schema 三种写法:
| 写法 | 适用 |
|---|---|
"type": ["string", "null"] | JSON Schema Draft 6+,OpenAPI 3.1 |
"type": "string", "nullable": true | OpenAPI 3.0 专用 |
"anyOf": [{"type": "string"}, {"type": "null"}] | 通用但冗长 |
OpenAPI 3.1 已统一用类型联合,新项目直接用 ["string", "null"]。
enum:候选有限还是无界
经验阈值:候选 ≤ 10 + 在样本中重复出现 = 适合 enum。
判断维度:
- 业务上是状态机吗?(status: active / inactive / deleted)
- 候选会随业务增长吗?(color 不会爆,user_id 会爆)
- 样本里同候选重复多次吗?(重复表示候选稳定)
工具默认:不主动推 enum——风险太大。错的代价(“city: 北京”被推成 enum,所有非北京数据失败)远大于对的收益。
手动加 enum 的时机:
// ✅ 适合 enum
{ "status": "active" }
{ "status": "inactive" }
{ "status": "deleted" }
// schema:
{ "enum": ["active", "inactive", "deleted"] }
// ❌ 不适合 enum——候选太多 / 会增长
{ "city": "北京" }
{ "city": "上海" }
enum 留扩展空间:业务后续会新增候选时,考虑 oneOf + 留 string 兜底,或直接用 pattern。
数字:integer 还是 number?范围是多少?
两步判断:
整数性
- 全部样本都是整数 ≠ 字段总是整数
- 业务语义优先:“age” 必整数;“temperature” 可小数
- 浮点精度问题——金额绝不用 float,用 integer 存”分”或 string 存 decimal
// ❌ 危险
{ "amount": 19.99 } // float 精度可能算错
// ✅ 推荐
{ "amount_cents": 1999 } // integer
{ "amount": "19.99" } // string with decimal
范围
- 业务规则——
age0-150;latitude-90 到 90 - 留余量——
max_users: 10000不要写当前最大值,留 10 倍空间 multipleOf: 0.01限制小数位(金额用)- JS Number 只能精确表示 ±2^53——超过用字符串
{
"type": "integer",
"minimum": 0,
"maximum": 150
}
{
"type": "number",
"minimum": -90,
"maximum": 90
}
字符串:除了类型还要约束什么
样本告诉你”是字符串”,schema 还要表达:
| 约束 | 用途 |
|---|---|
minLength / maxLength | 长度限制(防 DoS) |
pattern | 正则(手机号、车牌、特定 ID 格式) |
format | 标准化格式(email、uri、uuid、date-time) |
contentEncoding | base64 等编码 |
enum | 有限候选 |
工具默认:能识别 email、uri、uuid 等常见 format,不会自动加 length 和 pattern。
手动补刀:
- 用户输入字段加
maxLength——防 1MB 输入炸 DB - 业务 ID 加
pattern——^ORD-\d{12}$ - 标准格式加
format——校验时大多数库会启用
嵌套:什么时候该拆成 $ref
默认推荐:2-3 层;过深需要拆。
判断:
- 同一子结构在多处出现 → 抽成
$defs/Address,多处引用 - 业务上是独立实体(User、Order)→ 自然是独立 schema
- 数组元素结构复杂 → 把 items 抽出来独立命名
{
"$defs": {
"Address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" }
}
}
},
"properties": {
"billing_address": { "$ref": "#/$defs/Address" },
"shipping_address": { "$ref": "#/$defs/Address" }
}
}
远程 $ref 慎用——"$ref": "https://..." 会拖慢校验且依赖网络可达性。跨服务复用时复制到本地 + 标注来源 URL。
数组:元素结构如何并集
数组里元素结构不一致时,工具一般有两种策略:
// 样本
[
{ "type": "user", "name": "Alice" },
{ "type": "order", "amount": 100 }
]
策略 A:取并集(宽松)
{
"items": {
"type": "object",
"properties": {
"type": { "type": "string" },
"name": { "type": "string" },
"amount": { "type": "number" }
}
}
}
问题:name 和 amount 都成可选,校验等于没校验。
策略 B:oneOf(精确)
{
"items": {
"oneOf": [
{ "$ref": "#/$defs/User" },
{ "$ref": "#/$defs/Order" }
]
}
}
实务:异构数组用 oneOf——校验时强制每个元素必须严格匹配某个子 schema。
推断后的人工补刀清单
按顺序过一遍:
- required:删掉看着可选的字段,用 1000 条生产 sample 反向校验
- null 字段:每个都问”是值还是代表缺失”,分别处理
- enum 候选:扫一遍候选有限的字段,加 enum
- 数字范围:业务上有边界的全部加 minimum/maximum
- 字符串约束:用户输入加 maxLength;业务 ID 加 pattern;标准格式加 format
- 嵌套深度:被多处引用的子结构抽 $ref
- 异构数组:oneOf 而不是简单 items
- 未来扩展:留 buffer——maximum 留余量、enum 考虑后续新增
最后:schema 不是孤立资产
JSON Schema 应该和 TypeScript 类型、OpenAPI 文档联动维护:
- 选定一种 Source of Truth(OpenAPI / TS / JSON Schema)
- 其他都是自动生成产物
- CI 跑生成 + 检查产物已提交
- 生成代码加注释
// AUTO-GENERATED — do not edit
互转工具速查:
| 方向 | 工具 |
|---|---|
| JSON Schema → TS | json-schema-to-typescript |
| TS → JSON Schema | ts-json-schema-generator / typia / zod-to-json-schema |
| OpenAPI → TS | openapi-typescript |
| 运行时校验 | Ajv / zod / valibot |
推断工具是起点,不是终点。把”机械事实”用工具一次推完,把”业务约束”用人工补一遍——schema 才能既准又稳。