这些前端新技术你很难再忽视了 —— 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 数据的标准化语言。
把 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属性; number、street_name和street_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 自动下载这个地址;title、description:元数据注解,本身不增加数据约束;type:约束数据类型,可用object、array、string、number、integer、boolean和null;properties:为对象的指定属性分别定义 Schema;仅写在properties中并不会让属性自动变成必填;required:列出必须存在的属性名;它不负责判断属性值是否非空,值的约束要在properties中定义;enum:值必须等于数组中的某一个值;minimum、maximum、exclusiveMinimum、exclusiveMaximum:约束数值范围;minLength、maxLength、pattern:约束字符串;additionalProperties:控制没有被properties或patternProperties覆盖的额外属性;默认允许,设置为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
}
它同时违反了 number 的 integer、street_name 的 minLength 和对象的 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 不是万能验证
常见的 format 有 date-time、email、hostname、ipv4、ipv6 和 uri。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
}
这表示数组必须包含一个字符串和一个整数,且不允许第三项。旧版本中的数组形式 items 和 additionalItems 不应直接复制到 Draft 2020-12。
5. 对象属性
除了 properties 和 required,还可以使用:
patternProperties:根据属性名正则匹配约束;propertyNames:约束属性名本身;minProperties、maxProperties:限制属性数量;dependentRequired:出现某个属性时要求另一个属性;dependentSchemas:出现某个属性时对整个对象附加 Schema;unevaluatedProperties:在组合 Schema 场景下约束尚未被其他子 Schema 处理的属性。
原文提到的 dependencies 在较新的 Draft 中已经被 dependentRequired 和 dependentSchemas 替代;新项目不应继续把 dependencies 作为首选写法。
六、组合、引用和递归
1. 组合关键字
allOf:数据必须同时满足所有子 Schema;anyOf:数据满足一个或多个子 Schema;oneOf:数据必须恰好满足一个子 Schema,重叠时容易产生歧义;not:数据不能满足给定 Schema;if、then、else:根据条件选择附加约束;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 关键字直接复制过去。
一个稳妥的接口流程是:
- 为每个请求和响应定义版本化 Schema;
- 服务端在入口校验不可信数据;
- 前端生成类型或手写类型,但仍在运行时处理未知数据;
- 在 CI 中校验 Schema 本身和合法/非法样例;
- 生成文档、Mock 和契约测试;
- 新增字段时优先保持向后兼容,删除或改变类型时升级接口版本;
- 确认生成器、验证器和 OpenAPI 工具支持同一个 Draft。
“Schema 驱动”不等于“Schema 自动解决所有接口问题”。鉴权、幂等、业务状态、数据库约束、速率限制和错误语义仍需要单独设计。
九、安全和性能边界
JSON Schema 能约束数据形状,但不是安全过滤器,也不是权限系统:
- 不能因为字符串通过
pattern就把它直接拼进 SQL、HTML、Shell 或 URL; - 不能用 Schema 代替服务端鉴权和资源所有权检查;
- 对上传 JSON 设置大小、深度、数组长度和字符串长度限制,防止资源耗尽;
- 不要无条件编译用户提交的 Schema,复杂正则、深层组合和递归可能造成 CPU 或内存压力;
- 验证错误返回给客户端时只暴露必要信息,详细路径和内部结构写入受控日志;
- 对
format、正则表达式和自定义关键字进行安全评估,避免不同语言实现差异。
如果 JSON 来自用户输入,仍然需要在输出到 HTML、SQL、日志、命令行或其他协议前使用对应上下文的安全编码和参数化 API。