技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTETC

这些前端新技术你很难再忽视了 —— JSON Schema

所属馆藏
Other
文件格式
Markdown
原始路径
Other/22-这些前端新技术你很难再忽视了 —— JSON Schema
本文目录10 个章节
  1. 一、什么是 JSON Schema
  2. 二、先看一个合法的 JSON
  3. 三、Draft 2020-12 的街道 Schema
  4. 四、合法和不合法的数据
  5. 五、常见约束关键字
  6. 六、组合、引用和递归
  7. 七、在 JavaScript 中校验
  8. 八、JSON Schema 与 API、OpenAPI 和 TypeScript
  9. 九、安全和性能边界
  10. 十、常用资料和工具

这些前端新技术你很难再忽视了 —— JSON Schema

Category(分类): Other Status: 已整理

新系列《这些前端新技术你很难再忽视了》包括 SolidJS、Svelte、Tauri、Bun、JSON Schema 等。每一种技术都可能影响前端工程、接口设计或工具链,值得了解其适用边界。

本篇带来 —— JSON Schema

原文主要介绍 JSON Schema 的结构和简单校验。本文保留原来的街道示例,并更新到 JSON Schema Draft 2020-12,补充运行时校验、OpenAPI、代码生成、版本兼容和安全边界。

JSON Schema 工作方式示意

一、什么是 JSON Schema

一句话概括:JSON Schema 是描述和校验 JSON 数据的标准化语言。

把 JSON 想象成“数据本身”,把 JSON Schema 想象成“描述数据形状和约束的规则”。它可以说明:

  • 数据必须是对象、数组、字符串、数字、布尔值还是 null
  • 对象有哪些属性、哪些属性必填、是否允许未知属性;
  • 字符串的长度、正则模式和可选的 format
  • 数值范围、数组长度、数组元素类型;
  • 枚举、常量、条件约束和多个模式的组合;
  • 如何用 $ref$defs 复用、递归引用模式。

原文用“JSON Schema 之于 JSON,就像 TypeScript 之于 JavaScript”帮助理解,这个类比有启发性,但不能当作严格等价:

  • TypeScript 主要在开发和编译阶段检查代码,类型信息通常不会自动留在运行时;
  • JSON Schema 主要描述 JSON 实例,可以由不同语言的验证器在运行时执行;
  • TypeScript 类型系统表达的函数、类、条件类型和行为,JSON Schema 不一定能表达;
  • JSON Schema 的某些 format 只是注解,不同验证器的默认断言行为也可能不同。

所以,JSON Schema 既可以作为运行时接口契约,也可以被工具转换成 TypeScript 类型、文档、表单或测试数据,但生成结果仍然需要人工审查。

二、先看一个合法的 JSON

原文示例中的对象键没有加双引号,严格来说不是 JSON,而是 JavaScript 对象字面量。标准 JSON 要求对象键使用双引号:

{
  "number": 10,
  "street_name": "唐宁街",
  "street_type": "Avenue"
}

仅看这段数据,我们仍然无法知道:

  • number 必须是整数还是可以是小数;
  • number 是否必须大于 0,是否有最大值;
  • street_name 是否允许空字符串、是否有长度限制;
  • street_type 是否只能是少数几个值;
  • 是否允许额外的 direction 属性;
  • numberstreet_namestreet_type 哪些必须出现。

这些问题可以交给 JSON Schema 描述。

三、Draft 2020-12 的街道 Schema

JSON Schema 有多个版本(Draft)。当前常用的标准版本是 Draft 2020-12。在 Schema 中写入 $schema,可以声明该 Schema 遵循哪个方言和元模式:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/schemas/street.schema.json",
  "title": "Street",
  "description": "街道地址的简化示例",
  "type": "object",
  "properties": {
    "number": {
      "type": "integer",
      "minimum": 1
    },
    "street_name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "street_type": {
      "type": "string",
      "enum": ["Street", "Avenue", "Boulevard"]
    },
    "direction": {
      "type": "string",
      "enum": ["N", "S", "E", "W", "NE", "NW", "SE", "SW"]
    }
  },
  "required": ["number", "street_name", "street_type"],
  "additionalProperties": false
}

关键字段解释

  • $schema:声明 Schema 使用的 Draft/方言,不是被校验数据的业务版本号;
  • $id:为 Schema 设置 URI 标识和解析 $ref 的基准 URI。它是标识符,不代表验证器一定会通过 HTTP 自动下载这个地址;
  • titledescription:元数据注解,本身不增加数据约束;
  • type:约束数据类型,可用 objectarraystringnumberintegerbooleannull
  • properties:为对象的指定属性分别定义 Schema;仅写在 properties 中并不会让属性自动变成必填;
  • required:列出必须存在的属性名;它不负责判断属性值是否非空,值的约束要在 properties 中定义;
  • enum:值必须等于数组中的某一个值;
  • minimummaximumexclusiveMinimumexclusiveMaximum:约束数值范围;
  • minLengthmaxLengthpattern:约束字符串;
  • additionalProperties:控制没有被 propertiespatternProperties 覆盖的额外属性;默认允许,设置为 false 才会拒绝;
  • items:在 Draft 2020-12 中通常用于约束数组中剩余项目的 Schema;元组场景使用 prefixItems

additionalProperties: false 并不是所有 API 都应该使用的默认值。严格拒绝未知字段有利于尽早发现拼写错误,但也会降低向后兼容性。对需要逐步增加字段的公共接口,可以暂时允许未知字段,或在组合 Schema 中使用 unevaluatedProperties 精细控制。

四、合法和不合法的数据

符合上面 Schema 的数据:

{
  "number": 1600,
  "street_name": "Pennsylvania",
  "street_type": "Avenue",
  "direction": "NW"
}

不符合的数据:

{
  "number": 1600,
  "street_name": "Pennsylvania",
  "street_type": "super-speed"
}

原因是 street_type 不属于枚举值。

下面的数据也不符合:

{
  "number": "1600",
  "street_name": "",
  "street_type": "Avenue",
  "unknown": true
}

它同时违反了 numberintegerstreet_nameminLength 和对象的 additionalProperties: false

五、常见约束关键字

1. 数字

{
  "type": "number",
  "minimum": 0,
  "maximum": 100,
  "multipleOf": 0.5
}

如果业务上只允许整数,应使用 integer,不要只写 number。金额、数量和 ID 是否允许小数,也应由业务规则明确,不要因为 JavaScript 的 number 类型就混为一谈。

2. 字符串

{
  "type": "string",
  "minLength": 1,
  "maxLength": 200,
  "pattern": "^[A-Z][A-Za-z0-9_-]*$"
}

正则表达式适合表达简单格式,不适合替代完整的业务解析。原文的手机号正则把 | 放进字符类,实际会把竖线当成可接受字符;而且手机号规则受国家、地区和运营商影响,不能用一个中国大陆正则代表所有手机号。

如果只是演示 11 位数字,可以写成:

{
  "type": "string",
  "pattern": "^[0-9]{11}$",
  "description": "仅用于演示,不代表完整的手机号校验规则"
}

3. format 不是万能验证

常见的 formatdate-timeemailhostnameipv4ipv6uri。Draft 2020-12 把 format 分成注解和断言词汇,具体验证器可能默认只记录提示,也可能开启严格断言。

因此:

  • 需要强约束时,查看验证器文档并显式开启对应格式校验;
  • format: "email" 不能代替业务邮箱验证、验证码和账号状态检查;
  • format 不能代替权限校验、敏感词处理或 HTML 清洗。

4. 数组

{
  "type": "array",
  "items": { "type": "string" },
  "minItems": 1,
  "maxItems": 10,
  "uniqueItems": true
}

Draft 2020-12 的元组写法是:

{
  "type": "array",
  "prefixItems": [
    { "type": "string" },
    { "type": "integer" }
  ],
  "minItems": 2,
  "items": false
}

这表示数组必须包含一个字符串和一个整数,且不允许第三项。旧版本中的数组形式 itemsadditionalItems 不应直接复制到 Draft 2020-12。

5. 对象属性

除了 propertiesrequired,还可以使用:

  • patternProperties:根据属性名正则匹配约束;
  • propertyNames:约束属性名本身;
  • minPropertiesmaxProperties:限制属性数量;
  • dependentRequired:出现某个属性时要求另一个属性;
  • dependentSchemas:出现某个属性时对整个对象附加 Schema;
  • unevaluatedProperties:在组合 Schema 场景下约束尚未被其他子 Schema 处理的属性。

原文提到的 dependencies 在较新的 Draft 中已经被 dependentRequireddependentSchemas 替代;新项目不应继续把 dependencies 作为首选写法。

六、组合、引用和递归

1. 组合关键字

  • allOf:数据必须同时满足所有子 Schema;
  • anyOf:数据满足一个或多个子 Schema;
  • oneOf:数据必须恰好满足一个子 Schema,重叠时容易产生歧义;
  • not:数据不能满足给定 Schema;
  • ifthenelse:根据条件选择附加约束;
  • const:值必须严格等于某个常量;原文写的 consts 不是关键字。

例如,根据地址类型决定字段:

{
  "type": "object",
  "properties": {
    "country": { "type": "string", "enum": ["CN", "US"] },
    "postal_code": { "type": "string" }
  },
  "required": ["country", "postal_code"],
  "if": {
    "properties": { "country": { "const": "US" } }
  },
  "then": {
    "properties": {
      "postal_code": { "pattern": "^[0-9]{5}$" }
    }
  }
}

2. $defs$ref

可复用的子 Schema 可以放入 $defs,再通过 $ref 引用:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/schemas/customer.schema.json",
  "$defs": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 80
    }
  },
  "type": "object",
  "properties": {
    "first_name": { "$ref": "#/$defs/name" },
    "last_name": { "$ref": "#/$defs/name" }
  },
  "required": ["first_name", "last_name"]
}

$ref 的 URI-reference 会根据当前 Schema 的 base URI 解析。引用的 URI 不一定需要能被浏览器打开;验证器通常需要应用程序预先加载和注册对应的 Schema。递归树、目录和评论回复等数据也可以通过 $ref 指向自身。

七、在 JavaScript 中校验

浏览器端的 TypeScript 类型声明不能验证来自网络、表单、Local Storage 或第三方服务的运行时数据。前端可以做用户体验校验,但服务端必须在信任边界重新验证。

以 Ajv 为例,先安装验证器:

pnpm add ajv

Draft 2020-12 需要使用 Ajv 对应的实现入口,并根据项目构建方式调整导入:

import Ajv2020 from 'ajv/dist/2020.js'

const schema = {
  $schema: 'https://json-schema.org/draft/2020-12/schema',
  type: 'object',
  properties: {
    number: { type: 'integer', minimum: 1 },
    street_type: { enum: ['Street', 'Avenue', 'Boulevard'] }
  },
  required: ['number', 'street_type'],
  additionalProperties: false
}

const ajv = new Ajv2020({
  allErrors: true,
  strict: true
})

const validate = ajv.compile(schema)
const data = {
  number: 1600,
  street_type: 'Avenue'
}

if (!validate(data)) {
  console.error(validate.errors)
} else {
  console.log('valid')
}

验证器之间的默认行为可能不同,尤其是 Draft 版本、format、严格模式、未知关键字和类型推断。生产项目应固定验证器版本,明确使用的 Draft,并用合法/非法样例和边界数据写测试。

八、JSON Schema 与 API、OpenAPI 和 TypeScript

JSON Schema 很适合放在接口契约层:

Schema
 ├── 服务端请求校验
 ├── 服务端响应测试
 ├── OpenAPI 文档
 ├── 前端类型生成
 ├── 表单生成
 └── Mock 数据和契约测试

OpenAPI 3.1 的 Schema Object 与 JSON Schema 2020-12 更接近,并允许通过 jsonSchemaDialect 声明默认方言;OpenAPI 3.0 使用的是自己的一个子集,不能把所有 JSON Schema 2020-12 关键字直接复制过去。

一个稳妥的接口流程是:

  1. 为每个请求和响应定义版本化 Schema;
  2. 服务端在入口校验不可信数据;
  3. 前端生成类型或手写类型,但仍在运行时处理未知数据;
  4. 在 CI 中校验 Schema 本身和合法/非法样例;
  5. 生成文档、Mock 和契约测试;
  6. 新增字段时优先保持向后兼容,删除或改变类型时升级接口版本;
  7. 确认生成器、验证器和 OpenAPI 工具支持同一个 Draft。

“Schema 驱动”不等于“Schema 自动解决所有接口问题”。鉴权、幂等、业务状态、数据库约束、速率限制和错误语义仍需要单独设计。

九、安全和性能边界

JSON Schema 能约束数据形状,但不是安全过滤器,也不是权限系统:

  • 不能因为字符串通过 pattern 就把它直接拼进 SQL、HTML、Shell 或 URL;
  • 不能用 Schema 代替服务端鉴权和资源所有权检查;
  • 对上传 JSON 设置大小、深度、数组长度和字符串长度限制,防止资源耗尽;
  • 不要无条件编译用户提交的 Schema,复杂正则、深层组合和递归可能造成 CPU 或内存压力;
  • 验证错误返回给客户端时只暴露必要信息,详细路径和内部结构写入受控日志;
  • format、正则表达式和自定义关键字进行安全评估,避免不同语言实现差异。

如果 JSON 来自用户输入,仍然需要在输出到 HTML、SQL、日志、命令行或其他协议前使用对应上下文的安全编码和参数化 API。

十、常用资料和工具

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS