Skip to content
返回博客
教程

JSON Schema 验证完全指南:Draft 2020-12、Ajv、Python(2026)

在 Node、Python 和浏览器里用 JSON Schema 校验数据:Draft 2020-12 新特性、真实 API 实战、可复制示例,2026 完整指南。免费在线试用。

12 分钟

JSON Schema 验证:在 Node、Python 和浏览器里校验 JSON(2026)

速答:JSON Schema 是 JSON 数据的契约。你声明字段类型、必填键和约束,校验器据此判断任意一份 JSON 文档是否符合契约。Node 端用 Ajv 拿到最快的校验速度,Python 端用 jsonschema 库写跨语言可移植的 schema,浏览器端打包 Ajv 就能给表单和配置做即时反馈。2026 年新项目首选 Draft 2020-12。

接下来是最小可运行示例、三种运行时的端到端模式,以及那些会让「校验通过但生产环境拒绝数据」的真实陷阱。

JSON Schema 的边界

一句话定义

JSON Schema 本身就是一份 JSON 文档,用来描述其他 JSON 文档的形状。校验器读取 schema 与数据,确认两者是否一致,并返回失败的路径。

最小可用示例:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": { "name": { "type": "string" } },
  "required": ["name"]
}

{"name": "Alice"} 通过。{"age": 30} 失败(缺少 name)。{"name": 42} 失败(name 不是字符串)。心智模型就这么简单。

JSON Schema 与 JSON 语法校验

两个常被混淆的不同问题。

维度JSON 语法检查JSON Schema 校验
检查什么这是合法的 JSON 文档吗?这份 JSON 是否符合契约?
能捕获的问题缺逗号、单引号、注释类型错误、缺必填字段、值越界
工具JSON.parse()JSON 格式化工具Ajv、jsonschema(Python)、fastjsonschema
何时使用解析前的第一步解析后、业务逻辑前

实际工作里两步都得做:先用 JSON 格式化工具 美化负载确认能解析,再用 schema 校验是否符合契约。

JSON Schema 与 JSONPath、JSON Patch、jq、TypeScript

这五种工具占据相邻的问题空间。决策矩阵如下:

工具解决什么问题何时选用
JSON Schema这份 JSON 是否符合预期结构?校验 API 输入、配置文件、表单负载
JSONPath怎么从 JSON 里查询某个值?提取嵌套字段、批量读取
JSON Patch(RFC 6902)怎么描述 A 到 B 的差异?协作编辑、增量同步
jq怎么在命令行处理 JSON?Shell 脚本、日志管道、CI 检查
TypeScript 类型我的代码是否正确使用了这种形状?单一代码库内部的编译期保障

最关键的分界线是这一条:JSON Schema 在运行时校验未知数据,TypeScript 在编译时校验已知代码。第三方 webhook 推过来的 JSON、用户粘贴的 JSON,TypeScript 帮不上忙,那正是 JSON Schema 的用武之地。Zod 与 Pydantic 介于两者之间,既给编译期类型也跑运行时校验,下文展开。

JSON Schema 与 OpenAPI

一个常见误解:OpenAPI 替代了 JSON Schema。其实没有。OpenAPI 内部正是用 JSON Schema 来描述请求体和响应体,再在外面叠上路径、参数、安全方案和服务器 URL。schema 是数据形状契约,OpenAPI 是把它包起来的 API 契约。

维度JSON SchemaOpenAPI
范围单份 JSON 文档的形状整个 HTTP API 的形状
依赖无(schema 是自包含的 JSON)引入 JSON Schema 来描述请求/响应体
版本对应Draft 7 / Draft 2019-09 / Draft 2020-12OpenAPI 3.0 用 Draft 4 子集;OpenAPI 3.1 原生使用 Draft 2020-12
典型用途配置文件、消息封装、表单校验、单负载契约REST API 设计、SDK 生成、Mock 服务、契约测试
代码生成有限(少数 quicktype 类工具)生态成熟(openapi-generatoroapi-codegen、厂商 SDK)
契约管理一份 schema 对应一种形状,无路由路径、操作、鉴权流、版本化端点都集中在一份文档里

当你关心的产物只是一份单独的文档时,直接用 JSON Schema:webhook 负载、配置文件、队列消息、表单。没有 HTTP 表面要描述,OpenAPI 只是多余开销。

当你要发布一个 HTTP API,并希望一份文档同时驱动文档站、SDK 生成、Mock 服务和契约测试时,选 OpenAPI。先把 schema 作为独立的 JSON Schema 文件放在 schemas/ 目录,再在 OpenAPI 文档里通过 $ref 引用。这样 schema 在 API 之外仍可复用。

版本对应这件事最容易让团队踩坑。OpenAPI 3.0 用的是 Draft 4 子集,所以你不能在 3.0 文档里使用 prefixItemsunevaluatedProperties 这类 Draft 2020-12 关键字,生成器会静默忽略它们。OpenAPI 3.1 是 Draft 2020-12 的超集,凡是 2020-12 合法的写法在 3.1 里都合法。如果可以选,目标对准 OpenAPI 3.1,所有 schema 一律按 Draft 2020-12 来写。

你的第一份 JSON Schema(5 分钟)

必备的关键字

下面这些就能覆盖 80% 的场景:

{
  "type": "object",
  "properties": {
    "id":       { "type": "integer", "minimum": 1 },
    "email":    { "type": "string", "format": "email" },
    "age":      { "type": "integer", "minimum": 0, "maximum": 150 },
    "tags":     { "type": "array", "items": { "type": "string" }, "minItems": 1 },
    "role":     { "enum": ["admin", "editor", "viewer"] },
    "metadata": { "type": "object", "additionalProperties": true }
  },
  "required": ["id", "email"],
  "additionalProperties": false
}

词汇表:

  • typestringnumberintegerbooleannullarrayobject
  • propertiesrequired:声明字段并标记哪些必须存在
  • enumconst:限定为固定集合或单个字面量
  • minimummaximummultipleOf:数值边界
  • minLengthmaxLengthpattern:字符串长度与正则
  • minItemsmaxItemsuniqueItems:数组形状
  • additionalProperties: false:拒绝未声明的键。输入契约一律开启

按使用场景看 JSON Schema 示例

上面这些关键字会在不同场景下以不同组合出现。这里给出几种有代表性的形状:

API 请求体——一个接受邮箱和密码的注册端点:

{
  "type": "object",
  "properties": {
    "email":    { "type": "string", "format": "email" },
    "password": { "type": "string", "minLength": 8, "maxLength": 128 }
  },
  "required": ["email", "password"],
  "additionalProperties": false
}

配置文件——把日志级别锁定在固定集合里的 logger 配置:

{
  "type": "object",
  "properties": {
    "level":  { "enum": ["debug", "info", "warn", "error"] },
    "output": { "type": "string", "default": "stdout" }
  },
  "required": ["level"],
  "additionalProperties": false
}

带条件规则的表单负载——当 accountType"business" 时,taxId 变为必填:

{
  "type": "object",
  "properties": {
    "accountType": { "enum": ["personal", "business"] },
    "taxId":       { "type": "string" }
  },
  "if":   { "properties": { "accountType": { "const": "business" } } },
  "then": { "required": ["taxId"] }
}

CSV 行的 JSON 记录——导出订单表的一行:

{
  "type": "object",
  "properties": {
    "orderId":   { "type": "string", "pattern": "^ORD-[0-9]{6}$" },
    "orderedOn": { "type": "string", "format": "date" },
    "totalUsd":  { "type": "number", "minimum": 0 }
  },
  "required": ["orderId", "orderedOn", "totalUsd"]
}

Webhook 事件封装——oneOftype 字面量分发,让每种事件变体拥有自己的负载形状:

{
  "oneOf": [
    { "properties": { "type": { "const": "order.created" }, "data": { "$ref": "#/$defs/order" } } },
    { "properties": { "type": { "const": "order.refunded" }, "data": { "$ref": "#/$defs/refund" } } }
  ]
}

这五个示例覆盖了团队在实践中要写的绝大多数 schema。挑最接近的那一个复制过来,把字段名换掉,关键字词汇表本身保持不变。

不装任何东西也能校验

把 schema 和负载粘到 ajv.js.orgjsonschemavalidator.net 的在线 playground,立刻得到结论。如果 JSON 本身看起来可疑,先丢进 JSON 格式化工具 走一遍。

在 Node.js 里用 Ajv 校验

安装与 12 行示例

Ajv 在第一次 compile 时把 schema 编译成一个优化过的函数,之后反复复用。

npm install ajv
import Ajv from "ajv";
const ajv = new Ajv();

const schema = {
  type: "object",
  properties: {
    name: { type: "string" },
    age:  { type: "integer", minimum: 0 }
  },
  required: ["name"]
};

const validate = ajv.compile(schema);
const data = { name: "Alice", age: 30 };

if (!validate(data)) console.log(validate.errors);
else                  console.log("OK");

切换到 Draft 2020-12

为了向后兼容,默认的 Ajv 构造函数仍然锁定在 Draft 7。要使用 2020-12,得显式选用:

import Ajv2020 from "ajv/dist/2020";
const ajv = new Ajv2020({ strict: true, allErrors: true });

这样 prefixItemsunevaluatedProperties$dynamicRef 才会启用。各自的作用见后文 Draft 2020-12 那一节。

启用 format 校验

这条让无数开发者翻车,是 Ajv 最常见的怪点:默认情况下 format: "email" 什么都不做。规范把 format 当作建议性,所以你必须手动注册 format 模块。

npm install ajv-formats
import addFormats from "ajv-formats";
addFormats(ajv);  // 现在 "format": "email" 才真正生效

跳过这一步,{"email": "not-an-email"} 也能通过一份要求 format: "email" 的 schema。生产环境一律装上 ajv-formats

在 Express 里实战

每条路由一个校验器,启动时编译完成:

import express from "express";
import Ajv2020 from "ajv/dist/2020";
import addFormats from "ajv-formats";

const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);

const validateUser = ajv.compile({
  type: "object",
  properties: {
    email: { type: "string", format: "email" },
    age:   { type: "integer", minimum: 13 }
  },
  required: ["email"],
  additionalProperties: false
});

const app = express();
app.use(express.json());

app.post("/users", (req, res) => {
  if (!validateUser(req.body)) {
    return res.status(400).json({ errors: validateUser.errors });
  }
  // ... 业务逻辑
  res.status(201).json({ ok: true });
});

最昂贵的单一错误:在请求处理函数里调用 ajv.compile(schema)。请在模块作用域里编译一次,复用返回的函数。每次请求都重新编译,吞吐量会下降 50 倍以上。

在 Python 里用 jsonschema 校验

安装与基础用法

pip install jsonschema
from jsonschema import validate, ValidationError

schema = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age":  {"type": "integer", "minimum": 0}
    },
    "required": ["name"]
}

try:
    validate(instance={"name": "Alice", "age": 30}, schema=schema)
    print("OK")
except ValidationError as e:
    print("FAIL:", e.message, "at", list(e.absolute_path))

Draft202012Validator 收集所有错误

validate() 遇到第一个错误就抛出。要一次性列出所有问题(适合表单返回),改用 iter_errors

from jsonschema import Draft202012Validator

validator = Draft202012Validator(schema)
errors = sorted(validator.iter_errors(instance), key=lambda e: e.path)
for err in errors:
    print(f"  - {'/'.join(map(str, err.absolute_path))}: {err.message}")

这样用户能一次性把所有问题改完,不必反复来回。

jsonschema 与 Pydantic 怎么选

两个都很强的 Python 库,解决两种不同的问题。

维度jsonschemaPydantic v2
Schema 形态一个 JSON 字典(schema 是数据)一个带类型注解的 Python 类
性能解释执行,比 Pydantic 慢 10–100 倍Rust 内核,目前最快
跨语言可移植是(同一 schema 在 JS、Go、Rust 都能跑)否(仅 Python)
FastAPI 与原生模型集成需要手动转换内建
Draft 2020-12 关键字($dynamicRef 等)完整支持部分支持

生产环境验证过的经验法则:跨语言契约(OpenAPI、公开 API、webhook)用 jsonschema,内部 Python 服务用 Pydantic。两者并用也常见:网关层用 jsonschema 强制契约,应用层用 Pydantic 做类型化的业务逻辑。schema 是可移植的产物,跟你喂给 Ajv 的那份完全一样。

在浏览器里校验

客户端为什么也要校验

按重要性排序,三个理由。第一是 UX,用户输入时即时反馈,比来回服务器更顺。第二是带宽,明显错误根本不出浏览器。第三是安全卫生,可以减少打到后端的垃圾流量,但不替代服务端校验。

千万别只信任客户端校验。服务端必须再校验一次。

在浏览器里打包 Ajv

npm install ajv ajv-formats
import Ajv2020 from "ajv/dist/2020";
import addFormats from "ajv-formats";

const ajv = new Ajv2020({ allErrors: true });
addFormats(ajv);

export const validateForm = ajv.compile({
  type: "object",
  properties: {
    email:    { type: "string", format: "email" },
    password: { type: "string", minLength: 8 }
  },
  required: ["email", "password"]
});

打包体积大约多出 30 KB(gzip 后),不算小,但远未到灾难级。希望前后端共用一份 schema 定义的团队,会愿意付这个成本。

更轻的选择:Zod 和 Valibot

如果你不需要 JSON Schema 生态,并且本身就在 TypeScript 里,TS 原生校验器能给你更小的包体和更紧的类型推断:

import { z } from "zod";
const UserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
});
const result = UserSchema.safeParse(data);
if (!result.success) console.log(result.error.issues);

Valibot 体积大约 3 KB(gzip 后),API 类似,对包体最敏感时它最合适。但要注意,这两个库都不产出 JSON Schema。如果你需要一份能跟后端、第三方客户端或 OpenAPI 生成器共享的单一来源,请坚持用 Ajv。如果整条链路都是你自家的 TypeScript,Zod 和 Valibot 更顺手。

Draft 2020-12 带来什么

prefixItems 校验元组

Draft 7 通过 items: []additionalItems 来表达元组。Draft 2020-12 把它们干净拆开:

{
  "type": "array",
  "prefixItems": [
    { "type": "string" },
    { "type": "number" }
  ],
  "items": false
}

["x", 42] 通过,["x", 42, "extra"] 失败。schema 读起来跟它实际做的事情完全一致。

unevaluatedProperties 处理组合 schema

每个用 allOfoneOf 的团队都踩过这个微妙的坑:additionalProperties: false 只检查它出现的那一层。allOf 内部的兄弟子 schema 想声明什么属性都行。2020-12 的解法是 unevaluatedProperties: false

{
  "allOf": [
    { "$ref": "#/$defs/base" }
  ],
  "unevaluatedProperties": false
}

它会拒绝所有未被任何分支评估过的属性,这才是大多数开发者本来期待 additionalProperties: false 该有的行为。

$dynamicRef 描述递归 schema

在 Draft 7 里写过递归树 schema 的人,应该都熟悉那种扭来扭去的痛苦。$dynamicRef$dynamicAnchor 把它清理干净:

{
  "$dynamicAnchor": "node",
  "type": "object",
  "properties": {
    "value": { "type": "string" },
    "children": { "type": "array", "items": { "$dynamicRef": "#node" } }
  }
}

递归声明清晰,子孙 schema 也可覆写,无需重写 $id

Draft 7 还是 2020-12

新项目、现代工具链选 Draft 2020-12。构建或消费 OpenAPI 3.1 时它是原生方言。还在跟 OpenAPI 3.0 或更老的服务打交道,就用 Draft 4(OpenAPI 3.0 用的是 Draft 4 子集,切勿混合方言)。需要广泛的校验器兼容(Postman、老旧 CI 工具)时,Draft 7 仍是最稳妥的交换格式。

如今所有现代校验器(Ajv、Python jsonschemajsonschema-rs、Java 的 networknt/json-schema-validator)都已支持 2020-12。

真实世界的实战模式

API 输入校验

上面那段 Express 中间件就是生产级形态。再补两条实践:把所有 schema 集中在仓库根目录的 schemas/ 文件夹,并在 CI 里加一步 ajv test(或 Python 等价命令),让 schema 自身也用 JSON Schema 元 schema 校验一次。

配置文件

Visual Studio Code 内置了 SchemaStore 集成,package.jsontsconfig.json 这类几十种文件都能自动补全和内联校验。在你自己的配置里加一个 $schema 字段,编辑器用户也能享受同样的待遇。

CI 测试夹具

测试夹具会腐烂。有人改了模型,夹具还停留在旧形状,测试照样通过,因为断言压根没碰那个改过的字段。在断言之前加一道 schema 检查,就能拦住它:

import { glob } from "glob";
const files = await glob("__tests__/fixtures/*.json");
for (const f of files) {
  const data = JSON.parse(await fs.readFile(f, "utf8"));
  if (!validate(data)) throw new Error(`${f}: ${ajv.errorsText(validate.errors)}`);
}

schema 检查报警之后,下一步通常是看结构差异。把夹具丢进 JSON 对比工具,跟一份新鲜的生产样本对照,看看哪里漂移了。如果 timestamps 和 IDs 把 diff 刷屏,按 JSON Diff 中忽略 timestamps 与 IDs 的快照路径忽略模式 把信号从噪音里分离出来。

Webhook 负载(Stripe、GitHub)

第三方 webhook 是最值得用 JSON Schema 守护的地方之一。webhook 是契约,服务方可能修改契约,你想第一时间知道。Stripe 和 GitHub 都发布 OpenAPI 描述,可以从中抽出 JSON Schema。校验入站事件,破坏性升级会立刻在监控里亮灯,而不是悄悄污染状态。

Schema 驱动的表单校验

React Hook Form 提供了 @hookform/resolvers/ajv 适配器,Vue 的 VeeValidate 也有等价的 Ajv 插件。两者都能用同一份 JSON Schema 驱动表单渲染、错误提示和提交校验。schema 是单一来源,UI 自动继承它的规则。

友好的错误信息

默认信息为什么粗糙

开箱即用时,Ajv 会产出诸如 #/properties/email format must match "email" 这样的错误。给工程师调试 400 还行,给填结账表单的用户看就是天书。

ajv-errors 自定义信息

npm install ajv-errors
import ajvErrors from "ajv-errors";
ajvErrors(ajv);

const schema = {
  type: "object",
  properties: { email: { type: "string", format: "email" } },
  required: ["email"],
  errorMessage: {
    properties: { email: "请输入有效的邮箱地址" },
    required: { email: "邮箱不能为空" }
  }
};

errorMessage 关键字写在 schema 里,校验规则和面向用户的文案就能一起迁移。

ajv-i18n 翻译错误信息

ajv-i18n 提供 30 多种语言对默认信息的翻译。启动时加一行,校验器就能讲西班牙语、法语、日语,或你支持的任意语种。当你的 errorMessage 没覆盖到所有约束时,它是不错的兜底。

把 schema 路径映射到表单字段

每个 Ajv 错误都带一个 instancePath,形如 /users/0/email。多数表单库期望的是点号路径,比如 users[0].email。一行就能搞定:

const fieldPath = error.instancePath.replace(/^\//, "").replace(/\//g, ".");

Python 的 jsonschema 里对应的是 error.absolute_path,用 . 拼接即可达到同样效果。

五个能通过校验却让生产崩溃的陷阱

1. format 默认是建议性的

不装 ajv-formats、也不调用 addFormats(ajv),每个 format 关键字都形同虚设。{"format": "email"} 会接受 "not-an-email"。生产环境一律装 format 包。

2. additionalProperties 默认是 true

不写 additionalProperties: false,schema 会接受所有未声明的字段。客户端可以塞额外字段绕过校验。把 additionalProperties: false 设为输入契约的默认,需要时再有意识地放宽。

3. additionalProperties 不能组合

allOfoneOfanyOf 里,additionalProperties: false 只检查它自己那一层的属性。兄弟子 schema 全能溜过去。Draft 2020-12 的解法是 unevaluatedProperties: false

4. 远程 $ref 是生产隐患

$ref: "https://example.com/schema.json" 会让 Ajv 在第一次编译时通过网络拉取。这意味着延迟、远端卡死时的 DoS 风险,以及 MITM 攻击面。把所有 $ref 目标内联,或者在构建期从磁盘加载。

5. 生成的 schema 会跟真实数据漂移

quicktype、typescript-json-schema 这类工具能从已有类型生成 schema,但产出通常过于宽松:每个字段都可选、additionalProperties 大开。把生成的 schema 当作草稿,手动收紧;并在 CI 里用真实生产样本互相校验(数据 vs schema、schema vs 数据),让漂移尽早暴露。

性能:数字与经验法则

  • Ajv(Node.js)。编译过的校验器单次校验远低于 1 微秒,目前 JS 阵营里最快。
  • jsonschema(Python)。解释执行,比 Pydantic 慢 10–100 倍。被这一点拖累时换上 fastjsonschema,它会生成 Python 代码,性能逼近 Ajv。
  • Rust 与 Go。jsonschema-rsxeipuuv/gojsonschema 在网关层比 Ajv 再快 2–5 倍。
  • 最大的单一收益是预编译。模块加载时调用一次 ajv.compile(schema),每个请求复用返回的校验器。每次请求重新编译会让吞吐下降 50 倍以上。

常见问题

用大白话讲,什么是 JSON Schema 验证?

JSON Schema 验证就是检查一份 JSON 文档是否遵守某个契约。契约(即 schema)本身也是 JSON,声明类型、必填字段和约束。校验器读取 schema 与数据,报告「通过」,或者返回失败的路径与原因。

怎么在线把 JSON 跟 schema 校验?

把 schema 和数据粘到 ajv.js.org 的 playground 或 jsonschemavalidator.net,立刻有结论。如果 JSON 看起来格式有问题,先在 JSON 格式化工具 里整理一下。两者都在浏览器里运行,不会上传数据。

2026 年最快的 JSON Schema 校验器是哪个?

在 Node 里,预编译的 Ajv 单次校验低于 1 微秒。在 Python 里,fastjsonschema 通过生成代码达到 Ajv 级吞吐。网关层 jsonschema-rs(Rust)和 gojsonschema(Go)比 Ajv 再快 2–5 倍。无论选哪个,都要预编译一次然后复用。

JSON Schema 与 TypeScript 类型有什么区别?

TypeScript 在编译期检查你写的代码。JSON Schema 在运行期检查未知 JSON。HTTP 响应、文件、用户粘贴里飘来的 JSON,TypeScript 看不见,那正是 JSON Schema 要解决的。

Draft 2020-12 还是 Draft 7?

2026 年的新项目选 Draft 2020-12。prefixItemsunevaluatedProperties$dynamicRef 都解决了真实问题。OpenAPI 3.1 原生使用 2020-12。只有为了兼容 Postman 或老旧服务才留在 Draft 7。OpenAPI 3.0 用的是 Draft 4 子集,切勿混合方言。

怎么从已有 JSON 生成 JSON Schema?

三条路:把样本粘到 quicktype.io 或 jsonschema.net;命令行运行 npx genson-jspip install genson && genson sample.json;或者手写。自动生成的 schema 太宽松(每个字段可选、additionalProperties: true),当作契约前一定要收紧。

JSON Schema 能取代 OpenAPI 吗?

不能。OpenAPI 内部用 JSON Schema 描述请求和响应体,再叠上路径、安全方案、参数和服务器 URL。两者是组合关系:写好 schema、在 OpenAPI 文档里引用它们,就得到完整的 API 契约。

JSON Schema 跟 JSONPath 或 jq 是一回事吗?

不同问题。JSON Schema 校验结构(「这份 JSON 是否符合契约?」)。JSONPath 与 jq 提取值(「Running 阶段的所有 pod 名称」)。校验用 schema,查询用 JSONPath 或 jq。

为什么我的 Ajv 校验通过了,生产却拒绝数据?

三个元凶能解释几乎所有情况:忘了加 ajv-formats,于是 format: "email" 从未真的校验;漏写 additionalProperties: false,让客户端的额外字段溜了进去;在 allOfoneOf 里使用 additionalProperties: false,结果发现它不能组合。换成 unevaluatedProperties: false

能为终端用户定制 JSON Schema 错误信息吗?

可以。在 Node 里安装 ajv-errors,把 errorMessage 嵌入 schema;用 ajv-i18n 获得 30 多种语言的翻译。在 Python 里,jsonschema 会把完整的校验上下文挂在每个错误对象上,你可以把错误类型加路径映射到设计系统对应的文案。