JsonMind
API 与接口10 分钟阅读更新于 2026年11月10日

JSON Schema 校验失败:常见错误与修复方法

定位 JSON Schema 校验错误,理解 required、type、additionalProperties 与 format 的差异,并在数据进入业务逻辑前完成校验。

JM作者:JsonMind Engineering最后复核: 2026年11月10日依据权威规范与引用来源复核。
快速答案

JSON Schema 报错时先看实例路径,再看失败的关键字:required 表示缺少属性,type 表示 JSON 类型不符,additionalProperties 表示出现了未声明的字段,format 默认通常只是注解。确认路径对应的嵌套层级,再决定改数据还是改 Schema。

核心要点
  • 先读实例路径,它精确指向出错的值,比错误文案更可靠。
  • required、type、enum、pattern 和 additionalProperties 覆盖了大多数校验失败。
  • format 默认是注解,不要假设验证器会拒绝非法邮箱或日期。
  • 不同 Schema 草案支持的关键字不同,使用前先确认草案与验证器版本。
  • 即使有 TypeScript 类型,运行时边界仍然需要 Schema 校验。

JSON Schema 的校验错误通常很精确。错误文案可能很短,但实例路径和失败的关键字已经指出了要检查的值。真正困难的是判断问题出在 Schema 还是数据上。

本文按开发者最常遇到的错误类型,给出定位顺序和修复方式。

先读实例路径

验证器通常返回类似这样的路径:

#/items/0/price: must be number

路径指向实例而不是 Schema。它表示 items 数组中第一个元素的 price 属性没有通过 type 校验。先定位到这个值,再读关键字:

关键字检查内容
required对象缺少某个必需属性
typeJSON 类型不匹配
enum值不在允许范围内
pattern字符串不匹配正则
additionalProperties对象包含 Schema 未允许的键
minItems / maxItems数组元素数量不足或过多
oneOf / anyOf / allOf组合 Schema 的某个分支失败

涉及组合关键字时,单看错误文案很容易误判,实例路径才是最可靠的锚点。

这条路径本质上是 URI fragment 形式的 JSON Pointer。#/customer/address/zip 逐层经过三个键,#/items/0 选中数组第一个元素。两个转义规则必须记住:键名里的 / 写成 1, 写成 0,所以名为 a/b 的属性会显示成 #/a1b。如果路径只有一个 #,说明整个文档本身没通过校验,常见于根节点的 type 判断。

不同验证器给出的结构不同。Ajv 的 instancePath 不带 #,同时提供 keyword、params 和 schemaPath;Python 的 jsonschema 抛出 ValidationError,absolute_path 是一个 deque,消息类似 '42' is not of type 'number'。日志里应保存这些结构化字段,而不是只存一句消息,因为消息会随验证器版本变化,不适合作为测试断言。

一次返回二十条错误时,先修最浅路径上的第一条再重跑。开启 allErrors 后,后面的错误往往只是第一条的连锁反应,逐条追会让排查绕远路。

required:层级放错了

最常见的错误是把 required 写在根对象上,而属性实际位于嵌套对象中:

{
  "type": "object",
  "properties": {
    "customer": {
      "type": "object",
      "properties": {
        "email": { "type": "string" }
      }
    }
  },
  "required": ["email"]
}

这个 Schema 要求根对象直接包含 email,而不是 customer 内部包含 email。正确写法是把 required 放进 customer 的 Schema,根对象只要求 customer:

{
  "type": "object",
  "properties": {
    "customer": {
      "type": "object",
      "properties": {
        "email": { "type": "string" }
      },
      "required": ["email"]
    }
  },
  "required": ["customer"]
}

看到 required 报错时,先确认路径指向哪个对象,再确认 required 数组是否属于该对象的 Schema。

Ajv 的报错通常是 {instancePath: "/customer", keyword: "required", params: {missingProperty: "email"}},Python 则是 'email' is a required property。要注意 required 不检查值的质量:空字符串、null、0 和 false 都算“存在”。如果这些值不可接受,应叠加 minLength、type 或 not 约束。

required 只对对象生效,实例是数组时会被忽略;它也不会从父 Schema 继承。字段必须出现但允许为空时,不要放进 required,而是写成 "type": ["string", "null"]。否则调用方只能传一个占位值来满足校验,契约反而更脏。

type:值能解析,但类型不对

JSON 有六种值类型:对象、数组、字符串、数字、布尔和 null。Schema 写 number 时,字符串 "42" 会被拒绝;Schema 写 string 时,数字 42 也会被拒绝。

不要为了让校验通过就把类型改成 ["string", "number"],除非接口确实接受两种类型。应该先确定契约:

  • 不参与运算的 ID 通常用字符串。
  • 数量和金额用数字,并单独决定精度策略。
  • 布尔值不要用 "true"、"false" 字符串代替,除非有明确原因。

会自动做类型转换的验证器可能接受错误类型并悄悄转换,这会掩盖契约漂移。API 边界应使用严格校验。

两个细节经常被忽略。integer 和 number 是两个关键字,1.0 因为数学值没有小数部分,会被判定为 integer,1.5 则报 must be integer。另一个是精度:JSON 没有整数类型,JavaScript 用 IEEE 754 双精度解析,超过 9,007,199,254,740,991 的整数在校验器看到之前就已经丢精度。订单号、雪花 ID 这类值应使用字符串,金额用最小货币单位或字符串表示,不要赌 64 位整数能完整穿过 JSON。

开启 coerceTypes 后,"42" 会被原地改成 42,你校验的对象已经不再是收到的对象。信任边界上应关闭类型转换。直接校验内存中的 JavaScript 对象时,还要显式拒绝 NaN 和 Infinity,它们不是合法 JSON,却可能在序列化之前进入验证器。

additionalProperties:出现了意外字段

JSON Schema 中 additionalProperties 的默认值是 true。如果 Schema 没有定义某个属性,也没有限制额外键,验证器会接受它。

{
  "type": "object",
  "properties": {
    "id": { "type": "string" }
  },
  "additionalProperties": false
}

加上 additionalProperties: false 后,除 id 以外的键都会失败。这能发现拼写错误和意外泄露的字段,但当接口先新增字段、Schema 后更新时,也会立刻报错。

应该有意识地使用这个关键字。如果对象本来就是开放的,应写清文档,并考虑改用 propertyNames 或基于 pattern 的约束。

常见报错是 Ajv 的 must NOT have additional properties,params.additionalProperty 指向具体键名;Python 会写 Additional properties are not allowed ('foo' was unexpected)。被 patternProperties 匹配到的键算作已求值,不会报错。真正隐蔽的坑在 allOf 里:某个分支写了 additionalProperties: false,却只声明自己的属性,于是另一个分支声明的字段被它判为多余。解决办法是在外层使用 2019-09 之后的 unevaluatedProperties: false,或者每个分支重复列出全部允许字段。

是否封口是兼容性决策。禁止未知字段能抓住拼写错误和客户端漂移,但接口新增可选字段时,旧版 Schema 会立刻拒绝新客户端。较稳妥的做法是过渡期内记录未知字段并返回警告头,确认没有调用方依赖后再切换成拒绝。

format 默认只是注解

Schema 可以写:

{ "type": "string", "format": "email" }

很多验证器把 format 当作注解,不会拒绝非法邮箱。部分草案和验证器支持格式断言,另一些需要显式开启选项。

如果邮箱或日期校验很重要,应把 format 与 pattern 结合使用,或在应用代码中验证。除非确认验证器开启了格式断言,否则不要假设非法值已经被拒绝。

2019-09 草案把 format 拆成注解词汇表和断言词汇表,验证器是否真的检查取决于它启用了哪一套。Ajv 需要额外安装 ajv-formats,而且它的 email 规则只是实用主义的形状检查,并非完整 RFC 5322。date-time 要求带时区偏移的 RFC 3339 时间,2026-11-10 10:00:00 会失败,2026-11-10T10:00:00Z 才会通过。

pattern 也有类似陷阱:它使用未加锚点的 ECMA-262 正则,所以 "[0-9]{4}" 能匹配 abc1234xyz。要求整个字符串匹配时应写成 ^[0-9]{4}$。电话号码、身份证号和邮编没有合适的标准 format,应交给专门的库,Schema 里的 format 只作为文档说明。

oneOf、anyOf 与 allOf

组合关键字很强大,也容易被误读:

  • anyOf:至少一个分支通过。
  • oneOf:恰好一个分支通过。
  • allOf:所有分支都通过。

oneOf 最容易产生令人困惑的错误。如果两个分支同时匹配,即使每个分支单独都能通过,实例仍然失败。错误信息可能列出所有分支的失败原因,看起来像是什么都没匹配上。

一个实用做法是用常量字段做区分:

{
  "oneOf": [
    {
      "properties": {
        "kind": { "const": "card" },
        "last4": { "type": "string", "pattern": "^[0-9]{4}$" }
      },
      "required": ["kind", "last4"]
    },
    {
      "properties": {
        "kind": { "const": "bank" },
        "iban": { "type": "string" }
      },
      "required": ["kind", "iban"]
    }
  ]
}

const 让分支互斥,oneOf 的行为就变得可预测。

识别报错形态能省很多时间。Ajv 会给出 must match exactly one schema in oneOf,并用 params.passingSchemas 标记结果:多个分支同时匹配时是数组,没有任何分支匹配时是 null。Python 在多个分支匹配时提示 is valid under each of ...。报错不会告诉你原本想要哪个分支,只能拿实例逐个分支对照。

最常见的错误是分支里漏写 required。properties 只校验实际出现的键,空对象 {} 能同时满足所有没有 required 或 additionalProperties 的分支,于是 oneOf 在空对象上失败。每个分支都要有 required;分支必须互斥时再加 additionalProperties: false。如果业务并不要求互斥,anyOf 更便宜、错误也更清楚,因为 oneOf 必须把所有分支都算完。

草案差异与 $ref

JSON Schema 有多个草案。unevaluatedProperties、prefixItems、$dynamicRef 等关键字在旧草案中不可用,验证器也可能只实现某个草案的子集。

排查“关键字不起作用”之前,先确认三件事:

  1. Schema 是否通过 $schema 声明了草案。
  2. 验证器是否支持该草案。
  3. 关键字是否被当作未知注解忽略。

$ref 解析是另一个常见故障点。$id 缺失或错误会让引用解析到错误的文档。Schema 应尽量靠近使用它的代码,并在部署前确认每个 $ref 都能解析。

草案要明确固定下来。draft-07 的 $schema 是 http://json-schema.org/draft-07/schema#,复用区叫 definitions;2020-12 使用 https://json-schema.org/draft/2020-12/schema,改叫 $defs。2020-12 把数组形式的 items 移到 prefixItems,dependencies 拆成 dependentRequired 和 dependentSchemas。draft-07 中与 $ref 并列的关键字会被忽略,2019-09 之后才会生效。$id 会改变相对引用的基准 URI,同一段片段嵌在不同位置可能解析出不同结果。

构建时把引用到的 Schema 收进 $defs,不要在请求处理过程中联网抓取;验证器里的网络调用会把延迟、DNS 和可用性变成校验失败。CI 中还应拿元 Schema 校验 Schema 本身。很多“关键字不起作用”最后都归结为 aditionalProperties 这类拼写错误——未知关键字是合法的注解,验证器不会报错。

在边界上校验

Schema 是运行时契约。TypeScript 类型会被擦除,无法单独保护网络边界。

const schema = await loadSchema("order.schema.json");
const result = validator.validate(schema, payload);

if (!result.valid) {
  throw new ValidationError(result.errors);
}

需要校验的位置包括:

  • 进入业务逻辑之前的请求体。
  • 信任第三方 webhook 字段之前。
  • 服务启动之前读取的配置文件。
  • 迁移之前读取的已存储 JSON。

JSON Schema 验证器可以在浏览器中检查数据与 Schema 是否匹配。JSON 转 JSON Schema 生成器可以从示例生成初稿,OpenAPI 查看器适合查看接口契约中的 Schema 组件。校验前可以先用 JSON 格式化工具检查原始数据。

Schema 应在进程启动时编译一次,而不是每个请求都编译。Ajv 按 Schema 对象缓存编译结果,编译本身才是开销大头,每请求重新读文件会直接拖高延迟。对外接口开启 allErrors,让调用方一次修完多个字段;配置文件则适合快速失败。错误响应里返回 instancePath、keyword 和 params,不要返回 schemaPath 或整份 Schema,后者会泄露内部结构。

校验不能代替资源限制。100 MB 的请求体在 JSON.parse 阶段就会阻塞事件循环,Schema 根本还轮不到执行,因此字节上限必须放在 HTTP 层。如果解析结果会合并进应用对象,应使用无原型对象,或直接拒绝 proto 和 constructor 键;JSON Schema 只把 proto 当作普通属性,挡不住原型污染。

如果契约主要是行为约束(限流、顺序、跨字段算术),载荷是数 MB 的二进制块,或者前后端由同一团队同时发布、生成验证器的维护成本高于收益,那么 JSON Schema 并不是合适工具。此时用带类型解码的解析器或手写边界检查,Schema 只保留作公开 API 的文档。

排查清单

  1. 读实例路径。
  2. 确认失败的关键字。
  3. 检查嵌套层级是否正确。
  4. 确认 JSON 类型。
  5. 检查 additionalProperties 与意外字段。
  6. 检查组合分支。
  7. 确认草案与验证器支持范围。
  8. 确认每个 $ref 都能解析。
  9. 用最小数据复现。
  10. 把失败用例加入测试。
  11. 每次只删一个关键字或字段,找到真正改变结果的最小改动。

校验错误不是噪音,它是在告诉你契约与数据在哪里出现了分歧。

常见问题

错误路径 #/items/0/price 是什么意思?

它指向实例中的具体位置:items 数组的第一个元素,以及该元素的 price 属性。应该修改这个位置的值,而不是重写整个文档。路径通常比人类可读的错误描述更有用。

明明写了属性,为什么还提示 required?

检查嵌套层级。required 作用于包含该属性的对象,而不是根对象。如果属性位于嵌套对象内,required 数组必须写在那个嵌套 Schema 里。

JSON Schema 会自动校验 format 吗?

很多验证器默认把 format 当作注解,不会拒绝格式错误的邮箱或日期。部分草案和验证器支持格式断言或需要显式开启。涉及邮箱、日期时间、URI 时应叠加 pattern 或在应用层校验。

oneOf、anyOf 和 allOf 有什么区别?

anyOf 只要一个分支通过即可;oneOf 要求恰好一个分支通过;allOf 要求所有分支都通过。oneOf 适合互斥结构,但多个分支同时匹配时错误信息会非常混乱。

有 TypeScript 类型还需要 JSON Schema 吗?

需要。TypeScript 类型在运行时会被擦除,无法校验网络输入。JSON Schema 可以在运行时验证数据,还能用于文档和代码生成。存在 API 契约时,应以 Schema 作为事实来源。

参考资料

把指南用起来

浏览全部工具

打开浏览器工具。输入内容保留在浏览器中,不会上传到业务服务器。

相关指南

全部指南