JSON Schema 基础:从语法到实战
JSON Schema 是什么?说白了,它是一套描述 JSON 数据结构的规则语言。你给出一份 JSON 数据,Schema 就能告诉你:这数据长对了没?
我在团队推行 API 治理时,第一件事就是让大家把接口的请求和响应都配上 Schema。为什么?因为口头约定太容易出错了——前端说「我传个 userId」,后端以为是个字符串,结果是个数字,线上直接崩。有了 Schema,这些破事能少一半。
4.1 JSON Schema 的语法骨架
一个最简的 Schema 长这样:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://example.com/user.schema.json",
"title": "User",
"description": "用户基础信息",
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["name"]
}
几个关键字段我解释一下:
- $schema:声明你用的是哪个版本的 Schema 规范。我个人习惯用 draft-07,生态最成熟。
- $id:这个 Schema 的唯一标识,类似身份证号。跨文件引用时全靠它。
- title / description:给人看的,不是给机器看的。但别小看它——我见过一个项目,接口文档和 Schema 描述对不上,排查了三天才发现是描述写错了。
- type:根节点的数据类型。API 响应体通常是 object。
- properties:对象里的字段定义。
- required:必填字段列表。注意,required 是个数组,不是布尔值。
4.2 关键字详解:你一定会用到的那些
JSON Schema 的关键字很多,但日常高频使用的其实就十几个。我按用途分了三类:
| 类别 | 关键字 | 作用 | 示例 |
|---|---|---|---|
| 类型约束 | type | 指定数据类型 | "type": "string" |
| 类型约束 | enum | 限定取值列表 | "enum": ["dev", "test", "prod"] |
| 数值校验 | minimum / maximum | 数值范围下限/上限 | "minimum": 0, "maximum": 100 |
| 数值校验 | exclusiveMinimum / exclusiveMaximum | 是否包含边界值 | "exclusiveMinimum": true |
| 字符串校验 | minLength / maxLength | 字符串长度范围 | "minLength": 1, "maxLength": 64 |
| 字符串校验 | pattern | 正则表达式匹配 | "pattern": "^[a-zA-Z0-9]+$" |
| 数组校验 | minItems / maxItems | 数组元素个数范围 | "minItems": 1, "maxItems": 10 |
| 数组校验 | uniqueItems | 元素是否必须唯一 | "uniqueItems": true |
| 对象校验 | required | 必填字段列表 | "required": ["id", "name"] |
| 对象校验 | additionalProperties | 是否允许额外字段 | "additionalProperties": false |
这里有个坑我要重点说——additionalProperties。默认情况下它是 true,意味着你的对象可以塞进任何没定义的字段。我曾经接手过一个老项目,接口返回里莫名其妙多了一个字段,前端没处理,页面直接白屏。后来我强制所有 Schema 都加上 "additionalProperties": false,这类问题再没出现过。
4.3 验证规则:数据到底合不合格?
验证规则是 Schema 的核心能力。我把它拆成四个维度:
4.3.1 类型验证
JSON Schema 支持六种基础类型:string、number、integer、boolean、array、object。还有一个 null,但我不建议你在 API 里用 null——它会让前端处理变得很麻烦。用 "type": ["string", "null"] 这种联合类型,不如直接约定缺省值。
4.3.2 格式验证
除了基础类型,Schema 还内置了一些格式校验:
{
"type": "string",
"format": "email" // 邮箱格式
// 还有:date-time, uri, ipv4, uuid 等
}
注意,format 的校验强度取决于你用的验证器。有些库只做格式检查,不做语义验证。比如 "format": "email",它只检查有没有 @ 符号,不会真的去发邮件验证。
4.3.3 组合验证
这是 Schema 最强大的地方。你可以用逻辑关键字组合多个规则:
{
"allOf": [
{ "type": "object" },
{ "required": ["id"] }
],
"anyOf": [
{ "properties": { "status": { "enum": ["active"] } } },
{ "properties": { "status": { "enum": ["inactive"] } } }
],
"oneOf": [
{ "required": ["email"] },
{ "required": ["phone"] }
]
}
- allOf:所有子规则必须同时满足。相当于 AND。
- anyOf:至少满足一个子规则。相当于 OR。
- oneOf:恰好满足一个子规则。相当于 XOR。
我在设计用户注册接口时用过 oneOf:用户可以用邮箱注册,也可以用手机号注册,但不能两个都填。这个场景用 oneOf 表达得非常清晰。
4.3.4 条件验证
if-then-else 结构让 Schema 有了「逻辑分支」的能力:
{
"type": "object",
"properties": {
"type": { "enum": ["personal", "company"] },
"taxId": { "type": "string" }
},
"if": {
"properties": { "type": { "const": "company" } }
},
"then": {
"required": ["taxId"]
},
"else": {
"not": { "required": ["taxId"] }
}
}
这个例子什么意思?如果 type 是 company,那么 taxId 必填;否则 taxId 不能出现。你想想看,如果没有条件验证,你得在后端写多少 if-else?
4.4 完整示例:一个用户管理 API 的 Schema
把上面这些知识点串起来,我们写一个真实场景的 Schema:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://api.example.com/v1/user/create",
"title": "UserCreateRequest",
"description": "创建用户接口的请求体",
"type": "object",
"additionalProperties": false,
"required": ["username", "email", "role"],
"properties": {
"username": {
"type": "string",
"minLength": 3,
"maxLength": 32,
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
"description": "用户名,字母开头,允许字母数字下划线"
},
"email": {
"type": "string",
"format": "email",
"description": "用户邮箱"
},
"role": {
"type": "string",
"enum": ["admin", "editor", "viewer"],
"description": "角色权限"
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150,
"description": "年龄,可选字段"
},
"tags": {
"type": "array",
"items": { "type": "string" },
"minItems": 0,
"maxItems": 10,
"uniqueItems": true,
"description": "用户标签,最多10个,不可重复"
}
}
}
这份 Schema 覆盖了类型、长度、正则、枚举、范围、数组约束,还禁用了额外字段。你拿它去校验数据,基本不会漏掉什么。
4.5 知识体系总览
下面这张图帮你理清 JSON Schema 的核心脉络:
4.6 避坑指南
最后分享几个我实战中遇到的坑:
- 不要滥用 allOf:allOf 里的子 Schema 如果有冲突,验证会直接失败。我曾经把两个互斥的 required 放进了 allOf,排查了半天才发现是逻辑矛盾。
- pattern 的性能问题:复杂的正则表达式在数据量大时可能成为瓶颈。我见过一个 Schema 用
.*开头匹配,结果验证耗时从 2ms 涨到 200ms。能用 minLength 解决的,就别上正则。 - 版本兼容性:draft-04 和 draft-07 的语法有差异。比如 exclusiveMinimum 在 draft-04 里是布尔值,在 draft-07 里变成了数字。升级时一定要跑回归测试。
- additionalProperties 的陷阱:它只检查当前对象的额外字段,不会递归检查嵌套对象。如果你要严格限制整个数据结构,每个嵌套对象都得单独设置。