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

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

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

## 核心要点

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

---

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

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

## 先读实例路径

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

```text
#/items/0/price: must be number
```

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

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

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

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

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

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

## required：层级放错了

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

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

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

```json
{
  "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 没有定义某个属性，也没有限制额外键，验证器会接受它。

```json
{
  "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 可以写：

```json
{ "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 最容易产生令人困惑的错误。如果两个分支同时匹配，即使每个分支单独都能通过，实例仍然失败。错误信息可能列出所有分支的失败原因，看起来像是什么都没匹配上。

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

```json
{
  "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 类型会被擦除，无法单独保护网络边界。

```ts
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 验证器](/tools/json-schema)可以在浏览器中检查数据与 Schema 是否匹配。[JSON 转 JSON Schema 生成器](/tools/json-schema-gen)可以从示例生成初稿，[OpenAPI 查看器](/tools/openapi-viewer)适合查看接口契约中的 Schema 组件。校验前可以先用 [JSON 格式化工具](/tools/json-formatter)检查原始数据。

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 作为事实来源。

## 参考资料

- [JSON Schema 官方规范](https://json-schema.org/specification)
- [Understanding JSON Schema](https://json-schema.org/understanding-json-schema/)
- [OpenAPI Specification](https://spec.openapis.org/oas/latest.html)
- [RFC 8259 - JSON 数据交换格式](https://www.rfc-editor.org/rfc/rfc8259)

---

- Canonical: https://www.jsonmind.com/zh-CN/guides/json-schema-validation
- Locale: zh-CN
- Published: 2026-11-10
- Updated: 2026-11-10
- Tools: json-schema, json-schema-gen, json-formatter, openapi-viewer