Schema 设计、版本与错误定位
本节目标
能用 $defs/$ref、组合和条件减少重复;为工具联合类型设计明确判别字段;用版本号与迁移函数处理不兼容变化;把验证错误转换为 RFC 6901 JSON Pointer、关键字和安全错误码。
先把复用单元放进 $defs
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.invalid/schemas/tool-suggestion-v1",
"$defs": {
"request_id": {
"type": "string",
"pattern": "^req-[0-9]{4}$"
}
},
"type": "object",
"properties": {
"request_id": {
"$ref": "#/$defs/request_id"
}
}
}$id给 Schema 一个基础标识;示例使用保留的.invalid域名,不会成为真实端点;$defs保存可复用子 Schema;#/$defs/request_id是文档内 JSON Pointer;- 远程
$ref会引入解析、缓存、版本和供应链边界,生产系统应预先注册可信 Schema,不让不可信 instance 控制任意 URL。
allOf、anyOf、oneOf 不是同义词
| 关键字 | 语义 | 常见用途 |
|---|---|---|
allOf | 所有子 Schema 都必须匹配 | 叠加独立约束;不是面向对象继承。 |
anyOf | 至少一个匹配 | 允许多个可重叠形状。 |
oneOf | 恰好一个匹配 | 互斥联合;分支重叠会导致意外失败。 |
not | 子 Schema 不得匹配 | 排除明确形状。 |
工具参数联合推荐使用清晰判别字段:
{
"oneOf": [
{
"properties": {
"tool": {"const": "search_notes"},
"arguments": {"$ref": "#/$defs/search_arguments"}
}
},
{
"properties": {
"tool": {"const": "send_email"},
"arguments": {"$ref": "#/$defs/email_arguments"}
}
}
]
}外层仍需 required 和 additionalProperties。若分支没有 const 等互斥条件,同一 instance 可能匹配两个分支,从而违反 oneOf。
用 if/then/else 表达局部条件
{
"if": {
"properties": {"mode": {"const": "write"}},
"required": ["mode"]
},
"then": {
"properties": {"requires_approval": {"const": true}}
}
}这能表达“声明为写操作时,配置必须要求审批”,但 requires_approval: true 仍只是配置声明。真实运行时的审批事实必须来自可信审批系统,不能由模型在参数里自报 approved: true。
additionalProperties 与组合的陷阱
additionalProperties 只知道同一 Schema object 中声明的 properties,复杂 allOf 组合中可能意外拒绝其他分支定义的字段。Draft 2020-12 提供 unevaluatedProperties 处理“已由其他子 Schema 评估的属性”,但实现支持与语义更复杂。
初学项目优先:
- 把外层共有字段集中在一个对象 Schema;
- 用判别字段只细化
arguments; - 对每个具体
arguments对象设置additionalProperties: false; - 为每个分支写成功和额外字段失败测试。
不要为了炫技堆叠组合;可读和可测试的契约更重要。
Schema 版本与数据版本是两个概念
$schema:JSON Schema 方言,例如 Draft 2020-12;$id:这份 Schema 资源的标识;- instance 中的
schema_version:你的业务数据格式版本。
{
"schema_version": 1,
"name": "meeting-assistant"
}业务版本发生不兼容变化时:
- 保留 v1 Schema 和测试;
- 定义 v1 → v2 的纯迁移函数;
- 先严格验证 v1,迁移,再验证 v2;
- 记录迁移来源和版本,不原地猜测字段含义;
- 设计回滚或至少保留原输入;
- 新写入只生成当前版本,旧读取在明确期限内兼容。
字段重命名、单位改变、枚举删除和 null 语义改变都可能是不兼容变化。只改 Schema 文件而不迁移数据,会把旧数据变成运行时事故。
错误定位使用 RFC 6901 JSON Pointer
JSON Pointer 把路径编码为 /tools/1/name。两个转义规则:
~→~0/→~1
根值的 pointer 是空字符串。项目函数:
from collections.abc import Iterable
def json_pointer(parts: Iterable[str | int]) -> str:
encoded: list[str] = []
for part in parts:
token = str(part).replace("~", "~0").replace("/", "~1")
encoded.append(token)
return "" if not encoded else "/" + "/".join(encoded)不要把 JSON Pointer 与 JSONPath 混淆:Pointer 是定位单个值的标准语法;JSONPath(RFC 9535)是查询表达式。
验证错误要排序、归一化和脱敏
验证器可能返回多个错误,遍历顺序不应成为不稳定 API。一个可控策略:
- 按 instance path 和 validator keyword 稳定排序;
- 对外只返回第一个错误,或返回有上限的错误数组;
- 记录
code=schema_validation、Pointer、keyword; - 不直接输出
ValidationError.message中的 instance 值; - 把详细诊断留在访问受控的开发环境,并做脱敏。
例如:
{
"status": "rejected",
"code": "schema_validation",
"pointer": "/arguments/limit",
"keyword": "maximum"
}这个报告说明修复方向,但没有复制查询内容或邮件正文。
Schema 测试应围绕边界与演进
每个重要字段至少测试:
- 最小合法值、最大合法值;
- 小 1、大 1;
- 缺失、
null、错误类型; - 未知字段;
- 每个联合分支;
- 会匹配多个/零个
oneOf分支的输入; - 旧版本、未知未来版本;
- Schema 本身能通过
check_schema; - 验证错误 Pointer 和 keyword 稳定且不含敏感值。
Schema 文件与应用业务检查必须共同测试。Schema 无法表达的“工具名唯一”“ID 在数据库中存在”“写操作已审批”要有独立单元测试。
厂商 profile 与完整规范
LLM 厂商的 strict/structured output 常只支持 JSON Schema 的一个子集,并可能额外要求所有字段列入 required、每个对象写 additionalProperties: false,或用 null 表达可选。MCP 的当前版本也规定自身工具 Schema profile。
这些都是动态实现约束,不是 Draft 2020-12 的通用语义。实践时:
- 先写领域契约;
- 明确目标方言;
- 为供应商编译/裁剪一个 profile;
- 在本地再次验证模型输出;
- 记录供应商文档获取日期和 Schema 子集限制。
常见错误与排查
- 把
$schema、$id、schema_version当成同一版本:分别记录方言、资源和业务格式。 oneOf分支重叠:增加const判别字段和重叠失败测试。- 让 instance 提供
$refURL:只从可信注册表加载 Schema。 - 直接回显第三方错误 message:转成受控 code/pointer/keyword。
- 迁移时直接覆盖原文件:先验证、迁移到新对象、验证新版本,再原子写入。
- 把厂商子集写成“JSON Schema 不支持”:标注实现与日期。
练习
- 用
$defs/$ref提取一个可复用request_id,并写合法/非法测试。 - 为
search_notes和send_email设计带const判别字段的oneOf。 - 构造一个同时匹配两个
oneOf分支的反例,再修改 Schema 使分支互斥。 - 设计 v1 字段
timeout到 v2timeout_seconds的迁移、验证和回滚流程。 - 把路径
items/a~b/2编码成 JSON Pointer 并验证转义。
自测
anyOf与oneOf的差别是什么?$schema和 instance 中schema_version分别控制什么?- 为什么
requires_approval: true不能证明审批已经发生? - 根值的 JSON Pointer 是什么?
- 为什么验证器原始错误消息不应直接进入普通日志?
小结与下一步
Schema 是版本化的结构契约,错误报告也是公共接口。下一节把它放进 API、LLM 和工具调用边界:API、LLM 与工具调用中的 JSON。返回 JSON 学习目录。
参考资料
资料复核日期:2026-07-14。