JSON Schema 与数据校验实践:从关键字体系到轻量校验器实现

为什么数据校验是工程刚需

每个后端开发者都遇到过这些场景:

  • API 收到客户端请求体,字段缺失、类型错、格式不对,导致后续逻辑崩溃
  • 配置文件(如 K8s ConfigMap、CI/CD pipeline)写错字段名,部署时才暴露
  • 表单提交的邮箱、URL、日期格式不合法,存入数据库后无法使用
  • 微服务间传递的 JSON 消息结构漂移,消费方解析失败

手动写 if/else 校验既繁琐又容易遗漏。JSON Schema 就是为解决这类问题而生的:用一份标准化的 JSON 文档描述数据结构,再用校验器自动验证任意 JSON 是否符合约束。

配套工具:JSON Schema 校验工具

一、JSON Schema 是什么

JSON Schema 本身也是一个 JSON 对象,用一组预定义的关键字(keyword)声明数据必须满足的约束。它由 IETF 标准化,主流版本有:

  • draft-07:当前最广泛使用的稳定版本(本工具与本文重点)
  • draft-06:引入 constexamples
  • 2019-09 / 2020-12:拆分为多个子规范,新增 prefixItemsunevaluatedProperties

一份最简单的 Schema:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["id", "name"],
  "properties": {
    "id": { "type": "integer", "minimum": 1 },
    "name": { "type": "string", "minLength": 1 }
  }
}

$schema 声明所用规范版本,type 限定根类型,required 列出必填属性,properties 定义每个字段的子约束。

二、类型系统:type / enum / const

type

type 可取单值或数组:

{ "type": "string" }
{ "type": ["string", "null"] }   // 允许字符串或 null

draft-07 支持 7 种类型:nullbooleanobjectarraynumberstringinteger。注意 integernumber 的子集5 同时满足两者,但 5.1 只满足 number

enum 与 const

enum 限定值必须在列表中,const 限定值必须严格等于某个值:

{ "enum": ["admin", "editor", "viewer"] }
{ "const": 42 }

enum / const 不限定类型,可跨类型比较。校验器用深度相等判断(对象、数组递归比较)。

三、对象约束

对象是 JSON Schema 中最丰富的约束场景:

{
  "type": "object",
  "required": ["id", "name", "email"],
  "properties": {
    "id": { "type": "integer", "minimum": 1 },
    "name": { "type": "string", "minLength": 2, "maxLength": 32 },
    "email": { "type": "string", "format": "email" }
  },
  "patternProperties": {
    "^x-": { "type": "string" }      // 以 x- 开头的键必须为字符串
  },
  "additionalProperties": false,      // 禁止未在 properties 中定义的额外属性
  "minProperties": 1,
  "maxProperties": 20
}
  • required:数组,列出必须存在的键名(不校验值,只校验键存在)
  • properties:键名 → 子 schema 的映射,精确匹配键名
  • patternProperties:正则 → 子 schema,匹配键名(如 ^x- 匹配所有自定义扩展头)
  • additionalProperties:处理未被 properties / patternProperties 匹配的额外属性。false 表示禁止,schema 表示必须满足该约束,缺省表示放行
  • minProperties / maxProperties:限定属性数量

校验顺序通常是:先检查 required,再遍历每个属性,对每个键依次尝试 properties、patternProperties、additionalProperties。

四、数组约束

数组约束分两种模式:

单 schema 模式(列表)

{
  "type": "array",
  "items": { "type": "string" },   // 所有元素都必须是字符串
  "minItems": 1,
  "maxItems": 10,
  "uniqueItems": true               // 元素两两不重复
}

元组模式(Tuple)

{
  "type": "array",
  "items": [
    { "type": "string" },           // 第 0 位必须是字符串
    { "type": "integer" },          // 第 1 位必须是整数
    { "type": "boolean" }           // 第 2 位必须是布尔
  ],
  "additionalItems": false           // 禁止超出元组长度的额外元素
}

uniqueItems: true 需要对数组元素做去重判断,实现上常用两两深度比较(O(n²),对常见规模足够),或用规范化字符串作为 Map 键(O(n),但需处理对象键顺序)。

五、字符串约束

{
  "type": "string",
  "minLength": 6,                    // 最短 6 个字符
  "maxLength": 128,                  // 最长 128 个字符
  "pattern": "^[a-zA-Z0-9_]+$",      // 必须匹配正则
  "format": "email"                  // 必须符合 email 格式
}

注意 minLength / maxLength 按 Unicode 码点计数,不是字节也不是 UTF-16 单元。JavaScript 中用 [...str].length 即可正确计数(含 Emoji、中文)。

format 校验

draft-07 定义了一批语义格式,常用 9 种:

format含义示例
date-timeRFC 3339 日期时间2026-07-06T12:00:00Z
dateYYYY-MM-DD2026-07-06
timeHH:MM:SS12:00:00
email邮箱dev@example.com
uri含协议头的 URIhttps://example.com
ipv4IPv4 地址192.168.1.1
ipv6IPv6 地址::1
uuidUUID550e8400-e29b-…
hostname主机名example.com

format 校验默认是提示性的(draft-07 规范允许实现选择是否启用),生产场景通常需要明确开启。本工具的 format 校验为启发式正则实现,覆盖常见用例,但边界用例(如闰秒、非典型时区)可能与 ajv 等严格实现存在差异。

六、数值约束

{
  "type": "number",
  "minimum": 0,                      // >= 0
  "maximum": 100,                    // <= 100
  "exclusiveMinimum": 0,             // > 0(draft-07 起为数值,draft-04 为布尔)
  "exclusiveMaximum": 100,           // < 100
  "multipleOf": 0.5                  // 必须是 0.5 的整数倍
}

注意 draft-07 中 exclusiveMinimum / exclusiveMaximum 是数值(如 "exclusiveMinimum": 0 表示严格大于 0),而 draft-04 中它们是与 minimum / maximum 配合使用的布尔修饰符。本工具按 draft-07 语义实现。

multipleOf 实现时需注意浮点精度:0.1 + 0.2 !== 0.3,所以判断 value % multipleOf === 0 不可靠,应用 Math.abs(value / multipleOf - Math.round(value / multipleOf)) < 1e-10 容差判断。

七、组合关键字:allOf / anyOf / oneOf / not

这四个关键字实现逻辑组合,是 Schema 复用的核心:

{
  "allOf": [                          // 必须同时满足所有子 schema
    { "type": "object" },
    { "required": ["name"] }
  ],
  "anyOf": [                          // 至少满足一个子 schema
    { "minimum": 0 },
    { "type": "string" }
  ],
  "oneOf": [                          // 恰好满足一个子 schema(不能多也不能少)
    { "type": "string" },
    { "type": "integer" }
  ],
  "not": { "type": "null" }           // 必须不满足该子 schema
}

实现要点:

  • allOf:对每个子 schema 递归校验,所有错误都收集(不短路)
  • anyOf:对每个子 schema 用临时错误列表校验,至少一个为空则通过;否则报「不满足 anyOf」
  • oneOf:统计通过的子 schema 数量,必须恰好为 1;多了报「实际匹配 N 个」
  • not:子 schema 校验通过则报错,否则通过

oneOf 是最易错的:很多人以为它等价于 anyOf,但 oneOf 要求互斥——如果两个子 schema 都通过,会报错。

八、$ref 引用与 Schema 复用

大型 Schema 会拆分定义并用 $ref 引用,避免重复:

{
  "definitions": {
    "Address": {
      "type": "object",
      "properties": {
        "city": { "type": "string" },
        "zip": { "type": "string" }
      },
      "required": ["city"]
    }
  },
  "type": "object",
  "properties": {
    "home": { "$ref": "#/definitions/Address" },
    "work": { "$ref": "#/definitions/Address" }
  }
}

#/definitions/AddressJSON Pointer# 表示文档根,/definitions/Address 是路径段。路径段中的 ~1 表示 /~0 表示 ~(JSON Pointer 转义规则)。

循环引用防护

Schema 可能自引用(如树形结构):

{
  "definitions": {
    "Tree": {
      "type": "object",
      "properties": {
        "value": { "type": "number" },
        "children": {
          "type": "array",
          "items": { "$ref": "#/definitions/Tree" }
        }
      }
    }
  }
}

校验器必须维护 $ref 解析栈,遇到栈中已有的引用时跳过,避免无限递归。本工具用 refStack: string[] 实现,每次解析 $ref 前检查是否已在栈中。

本工具的 $ref 限制

为保持轻量,本工具仅支持同文档内部引用(以 #/ 开头),不支持:

  • 跨文档外部引用(如 {"$ref": "user.json#/definitions/User"}
  • $id 定义的基准 URI
  • 动态引用($dynamicRef,2020-12 引入)

需要复杂引用的场景建议使用 ajv。

九、轻量校验器实现要点

本工具的校验器(src/utils/jsonSchema.ts,约 380 行)采用递归下降实现,核心思路:

  1. 入口函数 validate(instance, schema):返回 { valid, errors },errors 是结构化错误列表
  2. 递归函数 validateNode(instance, schema, path, root, refStack, errors)
    • path 是当前实例路径(JSON Pointer 风格),用于错误定位
    • root 是 schema 根节点,用于解析 $ref
    • refStack$ref 解析栈,防循环
    • errors 是共享的错误收集数组
  3. 关键字分发:按关键字类型(type/enum/数值/字符串/数组/对象/组合)顺序校验
  4. 类型短路type 不匹配时直接返回,避免后续关键字对错误类型值校验产生噪音

错误结构:

interface ValidationError {
  path: string;      // 实例路径,如 "/users/0/email"
  message: string;   // 中文错误消息
  keyword: string;   // 触发关键字,如 "type"、"required"
}

UI 层用关键字徽章(按类型分类着色)+ 路径 + 消息展示,点击错误项可高亮该路径。

十、与 ajv 对比与选型

ajv 是功能最完整的 JSON Schema 校验库,生态成熟。本工具与之对比:

维度本工具ajv
支持规范draft-07 核心子集draft-04 ~ 2020-12 全部
关键字覆盖约 80%(核心关键字)100%(含 if-then-elsedependenciescontainspropertyNames 等)
$ref仅同文档内部内部 + 外部 + $id 基准 URI
format9 种内置正则可插拔,支持自定义 format
异步校验不支持支持(用于异步 format)
编译优化无(每次递归)预编译为函数,性能高 10x+
bundle 体积约 14KB(gzip 4.5KB)约 130KB+(含 draft-07 + format)
适用场景浏览器端快速校验、教学演示服务端严格校验、生产 API、规范一致性

选型建议

  • 浏览器端轻量校验、demo、教学 → 本工具或自实现
  • 生产 API、需要严格规范一致性、复杂外部引用 → ajv
  • 需要编译优化、高频校验 → ajv(预编译)

十一、实战场景

1. API 请求体校验

Express 中间件示例:

const schema = {
  type: "object",
  required: ["username", "password"],
  properties: {
    username: { type: "string", minLength: 3, maxLength: 32 },
    password: { type: "string", minLength: 8 },
    remember: { type: "boolean" }
  },
  additionalProperties: false
};

app.post("/login", (req, res) => {
  const { valid, errors } = validate(req.body, schema);
  if (!valid) {
    return res.status(400).json({ errors });
  }
  // ...业务逻辑
});

2. OpenAPI / Swagger

OpenAPI 3.0 的 Schema 对象就是 JSON Schema draft-04 的子集(OpenAPI 3.1 完整对齐 draft-2020-12)。你可以直接用 OpenAPI 文档中的 Schema 定义校验 API 响应:

# openapi.yaml 片段
components:
  schemas:
    User:
      type: object
      required: [id, name]
      properties:
        id:
          type: integer
          minimum: 1
        name:
          type: string
          minLength: 1

components.schemas.User 取出作为 schema,对实际 API 响应做校验,能快速发现接口契约漂移。

3. 配置文件验证

K8s ConfigMap、CI/CD pipeline、应用配置文件都可用 JSON Schema 描述约束,在加载时校验,避免运行时才暴露错误:

{
  "type": "object",
  "required": ["database", "port"],
  "properties": {
    "database": {
      "type": "object",
      "required": ["host", "port"],
      "properties": {
        "host": { "type": "string", "format": "hostname" },
        "port": { "type": "integer", "minimum": 1, "maximum": 65535 }
      }
    },
    "port": { "type": "integer", "default": 8080 }
  }
}

十二、与工具矩阵联动

本工具与工具盒子中的其他工具形成 JSON 处理工具集:

典型工作流:

  1. JSON 格式化 美化从 API 返回的 JSON
  2. JSON Schema 校验 验证结构是否符合契约
  3. JSONPath 查询 提取关键字段做断言

总结

JSON Schema 是描述 JSON 数据结构的标准,用关键字声明约束,校验器自动验证。draft-07 核心关键字覆盖:

  • 类型:type / enum / const
  • 对象:required / properties / patternProperties / additionalProperties / minProperties / maxProperties
  • 数组:items / additionalItems / minItems / maxItems / uniqueItems
  • 字符串:minLength / maxLength / pattern / format
  • 数值:minimum / maximum / exclusiveMinimum / exclusiveMaximum / multipleOf
  • 组合:allOf / anyOf / oneOf / not
  • 引用:$ref(内部引用)

轻量校验器用递归下降实现,约 380 行覆盖 80% 场景;生产严格校验建议用 ajv。实战场景覆盖 API 校验、OpenAPI 契约验证、配置文件校验,是数据驱动的现代后端工程的基础设施。

立即体验:JSON Schema 校验工具 — 粘贴 Schema 与数据,实时查看校验结果与错误定位。