JSON Schema 是描述和评估 JSON 实例的一组词汇。它可以断言类型、必填字段、范围、模式和关系,也可以提供文档与工具所需的注解。但它不是认证系统、对象所有权检查、输入清洗器,也不能保证某个值对下游解释器安全。

核心要点

  • $schema 声明方言,并使用与方言匹配的验证器。draft-07、2019-09 和 2020-12 不是完全互换的功能集合。
  • 区分会接受或拒绝实例的断言,以及 titledescriptiondefaultreadOnlyexamples 等注解。
  • format 的行为依赖方言和验证器配置;只有在契约确实要求时才启用对应格式和严格性。
  • 先用 Schema 校验形状,再独立执行认证、授权、语义检查、规范化和副作用策略。
  • 有意识地使用 $defs$refallOfanyOfoneOfif/then/elseunevaluatedProperties,组合可能产生歧义或意外封闭。
  • 限制输入大小、深度、属性数、数组长度、正则复杂度、引用展开和错误输出;验证器本身也是攻击面。

方言、词汇与 $schema

$schema URI 标识解释关键字所使用的方言和元 Schema。2020-12 可以使用 draft-07 验证器不认识的词汇和关键字。应固定验证器版本,并在 CI 中测试精确 Schema。

json
{
  "$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 建立用于引用和注册表的身份,不自动代表验证器可以访问的网络地址。除非明确允许并固定来源,否则应禁止任意远程引用加载。

断言与注解

参与验证的断言包括:

  • typeenumconst
  • requiredpropertiespatternPropertiesadditionalProperties
  • itemsprefixItemscontainsminItemsmaxItems
  • minimummaximummultipleOf
  • allOfanyOfoneOfnot 和条件关键字。

titledescriptiondefaultexamplesdeprecatedreadOnlywriteOnly 主要是注解,用于表达意图或指导工具。default 不会仅凭 Schema 自动插入值,除非应用显式实现默认化。readOnlywriteOnly 也不会自动执行访问控制。

对象、未知属性与组合

additionalProperties: false 会关闭同一个 Schema 对象中没有声明的属性。使用 allOf 时,其他子 Schema 中声明的属性可能仍被视为额外属性,除非组合方式专门处理。2020-12 的 unevaluatedProperties 可以表达另一种边界,但必须理解组合 Schema 的注解收集过程。

公共 API 应明确属性策略:

  • 拼写错误或批量赋值危险时拒绝未知字段;
  • 需要前向兼容时,把扩展字段放进命名空间;
  • 只在文档化的转换策略下剥离未知字段,不能把它暗中当作安全决策;
  • 用有效、无效和扩展实例测试组合。

字符串、数字与格式

minLengthmaxLength 按方言定义的 Unicode 代码点计数,不一定等于用户感知字符或字节数。pattern 是正则表达式,语法和性能依赖验证器;应避免灾难性模式并限制输入长度。

JSON 数字在数据模型中没有固定精度,但运行时可能使用有限数值类型。标识符或金额需要精确十进制/整数语义时,应使用带精确模式的字符串,或使用保留数字文本的解析器再配合领域规则。

format 不一定是断言。验证器可能在没有启用格式词汇或选项时只把它当作注解,内置检查也可能较宽松。可交付邮箱、允许的主机名、可请求 URL 或业务时区等要求,应由独立语义检查完成。

数组与条件 Schema

2020-12 中,元组式数组使用 prefixItems,剩余元素使用 itemsuniqueItems 比较 JSON 值,不代表对象拥有唯一业务 ID;后者应由应用代码或专用词汇校验。

条件 Schema 必须避免条件空通过:

json
{
  "$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 引用稳定共享契约:

json
{
  "$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 和格式包版本,明确选择方言;除非这是刻意设计的边界,不要在校验时静默修改输入:

javascript
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 校验

使用与声明方言匹配的验证器类,并只在应用接受其语义时传入格式检查器:

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 是字符串,却不能证明已认证主体有权读取或修改该对象。它可以约束工具参数形状,却不能决定租户所有权、网络出口、文件权限、速率限制或副作用是否需要审批。

应分层处理:

  1. 认证调用者,建立可信租户/主体上下文;
  2. 用正确 Schema 校验不可信实例;
  3. 授权对象、字段、操作和副作用;
  4. 只在明确策略下规范化或转换;
  5. 使用预算、幂等、审计记录和输出校验执行。

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 才是精确的契约工具。用它拒绝结构错误或不符合契约的实例,然后独立执行授权、规范化、安全和业务规则。通过真实兼容性测试管理 Schema 版本,避免把校验误当成安全边界。