JSON Schema 是描述和评估 JSON 实例的一组词汇。它可以断言类型、必填字段、范围、模式和关系,也可以提供文档与工具所需的注解。但它不是认证系统、对象所有权检查、输入清洗器,也不能保证某个值对下游解释器安全。
核心要点
- 用
$schema声明方言,并使用与方言匹配的验证器。draft-07、2019-09 和 2020-12 不是完全互换的功能集合。 - 区分会接受或拒绝实例的断言,以及
title、description、default、readOnly、examples等注解。 format的行为依赖方言和验证器配置;只有在契约确实要求时才启用对应格式和严格性。- 先用 Schema 校验形状,再独立执行认证、授权、语义检查、规范化和副作用策略。
- 有意识地使用
$defs、$ref、allOf、anyOf、oneOf、if/then/else和unevaluatedProperties,组合可能产生歧义或意外封闭。 - 限制输入大小、深度、属性数、数组长度、正则复杂度、引用展开和错误输出;验证器本身也是攻击面。
方言、词汇与 $schema
$schema URI 标识解释关键字所使用的方言和元 Schema。2020-12 可以使用 draft-07 验证器不认识的词汇和关键字。应固定验证器版本,并在 CI 中测试精确 Schema。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/user-create",
"type": "object",
"required": ["name", "email"],
"properties": {
"name": { "type": "string", "minLength": 1, "maxLength": 100 },
"email": { "type": "string", "format": "email" }
},
"additionalProperties": false
}
$id 建立用于引用和注册表的身份,不自动代表验证器可以访问的网络地址。除非明确允许并固定来源,否则应禁止任意远程引用加载。
断言与注解
参与验证的断言包括:
type、enum、const;required、properties、patternProperties、additionalProperties;items、prefixItems、contains、minItems、maxItems;minimum、maximum、multipleOf;allOf、anyOf、oneOf、not和条件关键字。
title、description、default、examples、deprecated、readOnly 和 writeOnly 主要是注解,用于表达意图或指导工具。default 不会仅凭 Schema 自动插入值,除非应用显式实现默认化。readOnly 和 writeOnly 也不会自动执行访问控制。
对象、未知属性与组合
additionalProperties: false 会关闭同一个 Schema 对象中没有声明的属性。使用 allOf 时,其他子 Schema 中声明的属性可能仍被视为额外属性,除非组合方式专门处理。2020-12 的 unevaluatedProperties 可以表达另一种边界,但必须理解组合 Schema 的注解收集过程。
公共 API 应明确属性策略:
- 拼写错误或批量赋值危险时拒绝未知字段;
- 需要前向兼容时,把扩展字段放进命名空间;
- 只在文档化的转换策略下剥离未知字段,不能把它暗中当作安全决策;
- 用有效、无效和扩展实例测试组合。
字符串、数字与格式
minLength 和 maxLength 按方言定义的 Unicode 代码点计数,不一定等于用户感知字符或字节数。pattern 是正则表达式,语法和性能依赖验证器;应避免灾难性模式并限制输入长度。
JSON 数字在数据模型中没有固定精度,但运行时可能使用有限数值类型。标识符或金额需要精确十进制/整数语义时,应使用带精确模式的字符串,或使用保留数字文本的解析器再配合领域规则。
format 不一定是断言。验证器可能在没有启用格式词汇或选项时只把它当作注解,内置检查也可能较宽松。可交付邮箱、允许的主机名、可请求 URL 或业务时区等要求,应由独立语义检查完成。
数组与条件 Schema
2020-12 中,元组式数组使用 prefixItems,剩余元素使用 items。uniqueItems 比较 JSON 值,不代表对象拥有唯一业务 ID;后者应由应用代码或专用词汇校验。
条件 Schema 必须避免条件空通过:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["country"],
"properties": {
"country": { "type": "string" },
"postalCode": { "type": "string" }
},
"if": {
"properties": { "country": { "const": "US" } },
"required": ["country"]
},
"then": {
"required": ["postalCode"],
"properties": { "postalCode": { "pattern": "^[0-9]{5}(-[0-9]{4})?$" } }
}
}
如果 if 内没有 required,当 country 缺失时,properties 可能因为没有矛盾而通过。
引用与可复用定义
使用 $defs 保存本地可复用片段,用 $ref 引用稳定共享契约:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"nonEmptyName": { "type": "string", "minLength": 1, "maxLength": 100 }
},
"type": "object",
"properties": {
"name": { "$ref": "#/$defs/nonEmptyName" }
},
"required": ["name"]
}
引用解析必须有界且确定。不要让不可信 Schema 触发任意网络请求、无限递归或无界展开。
使用 Ajv 的 JavaScript 校验
在项目中固定 Ajv 和格式包版本,明确选择方言;除非这是刻意设计的边界,不要在校验时静默修改输入:
import Ajv from "ajv";
import addFormats from "ajv-formats";
const schema = {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {
id: { type: "string", minLength: 1, maxLength: 64 },
email: { type: "string", format: "email" }
},
required: ["id", "email"],
additionalProperties: false
};
const ajv = new Ajv({
strict: true,
allErrors: false,
removeAdditional: false,
});
addFormats(ajv);
const validate = ajv.compile(schema);
export function validateUser(input) {
const valid = validate(input);
return {
valid,
errors: valid ? [] : (validate.errors ?? []).map((error) => ({
instancePath: error.instancePath,
keyword: error.keyword,
params: error.params,
})),
};
}
不要在错误响应中返回完整请求、密码、令牌或包含密钥的值。校验成功只说明实例匹配了当前配置的断言。
Python 校验
使用与声明方言匹配的验证器类,并只在应用接受其语义时传入格式检查器:
from jsonschema import Draft202012Validator, FormatChecker
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {"type": "string", "minLength": 1, "maxLength": 64},
"email": {"type": "string", "format": "email"},
},
"required": ["id", "email"],
"additionalProperties": False,
}
validator = Draft202012Validator(schema, format_checker=FormatChecker())
errors = sorted(validator.iter_errors({"id": "u-1", "email": "bad"}), key=str)
for error in errors:
print(error.json_path, error.validator, error.validator_value)
库及其传递依赖必须固定并测试。格式检查器不证明邮箱可投递,也不证明 URL 可以安全请求。
Schema 校验不是授权
Schema 可以要求 objectId 是字符串,却不能证明已认证主体有权读取或修改该对象。它可以约束工具参数形状,却不能决定租户所有权、网络出口、文件权限、速率限制或副作用是否需要审批。
应分层处理:
- 认证调用者,建立可信租户/主体上下文;
- 用正确 Schema 校验不可信实例;
- 授权对象、字段、操作和副作用;
- 只在明确策略下规范化或转换;
- 使用预算、幂等、审计记录和输出校验执行。
Schema 演进与兼容性
改变 required、收窄枚举、减小上限、关闭额外属性或改变类型,都可能破坏现有生产者或消费者。新增可选属性通常更容易发布,但 additionalProperties: false 和生成客户端仍可能使其成为破坏性变更。
用明确兼容性策略管理版本:
- 向后兼容:旧实例仍被接受;
- 向前兼容:旧消费者可以处理新实例;
- 读写兼容:请求和响应规则可以不同;
- 迁移策略:默认值、强制转换和变换是否在校验外执行。
让旧/新 fixture 分别通过两版验证器,发布弃用窗口;Schema diff 不能单独证明 API 完全兼容。
错误、限制与可观测性
返回稳定的机器可读错误码和有界路径,不返回原始 payload 或堆栈。决定只报首个错误还是收集全部错误;allErrors 可能增加 CPU 并暴露更多数据。限制 Schema 大小、引用深度、实例深度、数组长度、属性数、正则成本和错误输出。
记录 Schema ID/版本、验证器版本、方言、策略选项、结果和关联 ID。默认不要记录密钥或完整无效实例。
常见问题
JSON Schema 会清洗输入吗?
不会。它只验证选定断言,不会删除 HTML、防止 SQL 注入、授权对象、验证所有权或让 URL 可以安全请求。解析和校验后仍需执行上下文相关控制。
default 会自动填充缺失值吗?
仅按 JSON Schema 规范不会。default 是注解。框架可能实现默认值插入,但该变更必须显式、可测试,并与纯校验分开。
format: email 能证明邮箱可用吗?
不能。它可能只做语法检查,具体行为依赖验证器和启用的格式词汇。可投递性、域策略、验证和滥用控制是独立问题。
为什么 if/then 会校验出意外结果?
当区分条件的属性缺失时,if 可能通过。应在条件内部加入 required,并测试缺失、匹配和不匹配三种情况。
additionalProperties: false 总是最安全吗?
不是。它可以捕获拼写错误,但也可能破坏前向兼容,并与 allOf 产生意外交互。应先选择扩展和演进策略,再测试组合 Schema。
一手来源
- JSON Schema 2020-12 Core
- JSON Schema 2020-12 Validation
- Understanding JSON Schema
- Ajv 文档
- python-jsonschema 文档
- OWASP:Input Validation Cheat Sheet
总结
当方言、词汇、断言、注解、引用和限制都明确时,JSON Schema 才是精确的契约工具。用它拒绝结构错误或不符合契约的实例,然后独立执行授权、规范化、安全和业务规则。通过真实兼容性测试管理 Schema 版本,避免把校验误当成安全边界。