4. Schema 定义与类型系统:JSON Schema 基础
好,咱们进入数据契约最核心的部分——Schema 定义。说白了,Schema 就是数据的「宪法」。你接口返回什么字段、字段是什么类型、能不能为空、取值范围是多少,全得靠它来约束。
我个人习惯把 Schema 看作「接口的接口」。为什么这么说?因为它是契约双方(服务端和客户端)共同遵守的规则。没有 Schema,你的接口文档就是一本「仅供参考」的小说,而不是可执行的规范。
4.1 JSON Schema 是什么?
JSON Schema 是一种基于 JSON 的声明式语言。它用来描述 JSON 数据的结构、类型和约束。你想想看,我们每天都在写接口、调接口,但怎么保证我给你的数据就是你想要的?JSON Schema 就是干这个的。
举个例子,一个最简单的 Schema:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer", "minimum": 0 }
},
"required": ["name"]
}
这段 Schema 说了三件事:第一,数据必须是个对象;第二,name 字段是字符串,age 是整数且不能小于 0;第三,name 是必填的。就这么简单直接。
$schema 字段,指定 Draft 版本。虽然不写也能用,但写了之后 IDE 和校验工具能给你更好的提示。我一般用 Draft-07,兼容性最好。
4.2 基本类型:string, number, object, array
JSON Schema 支持的类型其实不多,但够用。我按使用频率排个序:
| 类型 | 说明 | 常用约束 | 我在项目中踩过的坑 |
|---|---|---|---|
| string | 字符串 | minLength, maxLength, pattern, format | 忘记加 format: "email",结果客户端传了个 "abc" 过来 |
| number | 数值(含小数) | minimum, maximum, multipleOf | 用 integer 定义金额,结果丢了分 |
| integer | 整数 | minimum, maximum | 分页参数没设 minimum: 1,有人传了 -1 |
| object | 对象 | properties, required, additionalProperties | 没关 additionalProperties,客户端多传了个字段 |
| array | 数组 | items, minItems, maxItems, uniqueItems | 列表接口没设 maxItems,一次返回了 10 万条 |
| boolean | 布尔值 | 无特殊约束 | 有人传了字符串 "true",校验没通过 |
| null | 空值 | 通常配合 oneOf 使用 | 字段允许 null 但没写进 Schema,客户端直接崩了 |
嗯,这里要注意:number 和 integer 是分开的。我见过不少团队把年龄定义成 number,结果传了个 25.5 过来。虽然业务上不合理,但 Schema 校验是能过的。所以该用 integer 就用 integer。
4.2.1 string 的 format 约束
string 类型有个特别好用的东西叫 format。它相当于内置的「正则表达式快捷方式」:
{
"type": "string",
"format": "email" // 校验邮箱格式
// 还有: date, date-time, uri, ipv4, uuid 等
}
我曾经在项目中遇到过一个坑:用户注册接口的邮箱字段没加 format,结果有人传了个 "abc" 进来。系统没报错,但后续的邮件发送模块直接炸了。从那以后,我所有字符串字段都会问自己一句:「这个字段有没有标准格式?」
4.2.2 array 的 items 约束
数组的 items 有两种写法:
// 方式一:所有元素类型相同
"items": { "type": "string" }
// 方式二:元组模式,每个位置类型不同
"items": [
{ "type": "string" },
{ "type": "integer" },
{ "type": "boolean" }
]
方式一最常见,比如返回用户 ID 列表。方式二用得少,但处理固定长度的坐标对(如 [经度, 纬度])时特别好用。
additionalItems 默认是 true。如果你用了元组模式,记得把 additionalItems: false 加上,否则客户端可以在元组后面继续塞元素,Schema 不会报错。
4.3 枚举与常量
枚举(enum)和常量(const)是 Schema 里最「硬」的约束。它们把值限定在几个固定选项里,没有任何商量余地。
4.3.1 enum:只能选我列出来的
{
"type": "string",
"enum": ["pending", "approved", "rejected"]
}
这个 Schema 说:status 字段只能取这三个值,多一个不行,少一个也不行。我在项目中用 enum 定义状态机流转,效果特别好。客户端传 "pending" 就是 "pending",传 "PENDING" 就是非法——enum 是大小写敏感的。
4.3.2 const:就这一个值
{
"type": "string",
"const": "v1"
}
const 比 enum 更极端:它只允许一个值。我一般在版本号、固定标识符上用 const。比如 API 版本字段,写死了就是 "v1",谁也别想改。
4.4 组合模式:allOf, oneOf, anyOf
组合模式是 JSON Schema 最强大的部分。它让你能把多个 Schema「拼」在一起,形成更复杂的约束。我刚开始学的时候觉得有点绕,但用熟了之后发现,它几乎能描述任何数据结构。
4.4.1 allOf:必须同时满足
allOf 的意思是「所有子 Schema 都得通过」。我常用它来做「基础 Schema + 扩展 Schema」的模式:
{
"allOf": [
{ "$ref": "#/definitions/baseUser" },
{
"required": ["role"],
"properties": {
"role": { "enum": ["admin", "editor"] }
}
}
]
}
这段代码说:这个用户必须满足 baseUser 的所有约束,同时还得有 role 字段,且 role 只能是 admin 或 editor。说白了就是「继承 + 扩展」。
4.4.2 oneOf:只能满足一个
oneOf 是「排他性选择」——多个子 Schema 中,数据只能满足其中一个。我处理「多态」场景时必用它:
{
"oneOf": [
{
"type": "object",
"properties": {
"type": { "const": "text" },
"content": { "type": "string" }
},
"required": ["type", "content"]
},
{
"type": "object",
"properties": {
"type": { "const": "image" },
"url": { "type": "string", "format": "uri" },
"width": { "type": "integer" },
"height": { "type": "integer" }
},
"required": ["type", "url"]
}
]
}
这个 Schema 说:消息要么是文本类型(有 content),要么是图片类型(有 url、width、height),不能同时是两种。我曾经在即时通讯项目里用 oneOf 定义消息体,客户端和服务端对得严丝合缝。
4.4.3 anyOf:满足一个或多个
anyOf 比 oneOf 宽松——只要满足任意一个子 Schema 就行,可以同时满足多个:
{
"anyOf": [
{ "type": "string", "minLength": 1 },
{ "type": "number", "minimum": 0 }
]
}
这个 Schema 说:字段可以是非空字符串,也可以是非负数。两个都满足?也行。anyOf 适合「多种合法类型」的场景,比如搜索关键词可以是文字也可以是数字 ID。
4.5 组合模式对比
| 组合关键字 | 匹配规则 | 典型场景 | 我常用的技巧 |
|---|---|---|---|
| allOf | 全部匹配 | Schema 继承、字段扩展 | 配合 $ref 做基础 Schema 复用 |
| oneOf | 恰好匹配一个 | 多态、联合类型、状态机 | 加 discriminator 字段做区分 |
| anyOf | 至少匹配一个 | 宽松类型、可选格式 | 注意校验顺序,优先匹配严格规则 |
4.6 本章知识体系
下面这张图把本章的核心知识点串起来了。你可以看到,从基本类型到组合模式,是一层一层往上构建的:
4.7 实战建议
说了这么多,最后给几个我在项目中沉淀下来的建议:
- 从基本类型开始:别一上来就搞 allOf、oneOf。先把 string、number、object 这些基础类型用熟,再逐步引入组合模式。
- 能用 enum 就别用正则:enum 的语义更清晰,校验更快。只有 enum 覆盖不了的情况(比如邮箱格式)才用 pattern。
- 组合模式一定要加区分字段:用 oneOf 时,每个子 Schema 里加一个 const 字段做「标签」。这样校验器能快速定位到正确的子 Schema,性能更好,错误信息也更友好。
- 别忘了 additionalProperties:object 类型默认允许额外字段。如果你希望严格校验,记得加上
additionalProperties: false。我见过太多因为多传了一个字段导致数据不一致的案例了。