← 中文资源

简体中文版本

如何组合 JSON Schema

用 allOf 和 oneOf 组合 JSON Schema 定义校验复杂载荷,schema 可跨服务跨语言复用。

阅读英文原文

概述

本文讲解如何用 JSON Schema 解决 validate complex payloads 问题。核心思路是:在数据进入业务逻辑之前,用显式约束拒绝不合法的输入,让正常路径保持快速,让失败路径保持可见。

关键设计

Define base definitions 是第一个决策,它决定了后续 JSON Schema 管道能假设什么。错误的选择会在高负载下暴露为 validate complex payloads 问题,你以为 JSON Schema 保证的不变量不再成立。在承诺前先测量影响,因为错误选择的代价是事故而非代码评审。

Compose with allOf 的机制在 JSON Schema 中描述简单,但后果会扩散。一个错误的校验会在负载下变成数据问题,你原以为 JSON Schema 保证的不变量不再成立。

实现要点

代码先用 JSON Schema 定义约束,再在边界处调用校验。校验失败时收集所有错误路径,让用户一次看到全部问题,而不是逐个修复。测试用假时钟处理时间逻辑,用桩注入失败路径,断言用户可见的结果。

Branch with oneOf 是大多数 JSON Schema 实现泄漏性能的地方。一个看起来健康的平均值可能隐藏一个毁掉最慢百分之十用户体验的尾延迟。测量尾延迟,在尾延迟上告警,优化尾延迟,因为平均值会撒谎而尾延迟说出真相。

常见陷阱

  • 跳过 JSON Schema 边界检查,让 validate complex payloads 未校验即进入系统。
  • 假设 JSON Schema 很快,却不测量尾延迟。
  • 让 validate complex payloads 静默失败,而非带指标地显式失败。
  • 只测试正常路径,导致 JSON Schema 失败在生产中暴露。
  • 跨请求共享可变状态,一个坏输入毒化下一个。

验证方式

编写测试驱动 JSON Schema 到失败条件,断言系统优雅降级而非崩溃。用 node:test 或 vitest 运行,检查校验报告每个违规路径,且正常路径不被校验开销拖慢。这样 validate complex payloads 就是你维护的属性,而非你期望的功能。

迁移与演进

当契约需要变更时,采用扩展-收缩策略:先发布兼容新旧两种形状的版本,待所有生产者都迁移到新形状后,再发布拒绝旧形状的版本。JSON Schema 的版本化能力使这一过程可控,避免破坏性变更导致的生产事故。

Validate with Ajv 的难点不在正常路径,而在 JSON Schema 失败一半时会发生什么。枚举失败模式,为每种情况决定用户看到什么。提前决定的失败是降级,在生产中发现的失败是事故。用混沌测试注入你担心的故障,是在生产前发现接缝的最廉价方式。

可观测性

JSON Schema 的校验决策应当无需调试器即可观测。当 JSON Schema 出错时,运维应看到包含被拒绝输入、拒绝规则和所做决定的结构化日志。静默失败的 JSON Schema 是无法运维的。在决策点添加结构化日志,在拒绝点添加指标,在整个操作周围添加追踪跨度,使“为什么失败”在仪表板中有答案而非在重新部署中。

衡量 JSON Schema 的指标应是用户感受到的——尾延迟、错误率,而非易于绘图的平均值。看起来健康的平均值可能隐藏毁掉最慢百分之十用户体验的尾延迟。在尾延迟上告警,优化尾延迟,因为平均值撒谎而尾延迟说真话。

代码示例

const schema = {
  $ref: '#/defs/user',
  defs: { user: { allOf: [
    { type: 'object', required: ['id'] },
    { properties: { id: { type: 'string', format: 'uuid' } } },
  ]}}},
};
test('validates composed user', () => {
  const v = new Ajv().compile(schema);
  expect(v({ id: crypto.randomUUID() })).toBe(true);
});