4. 数据模型与序列化:Schema定义、数据类型约束、嵌套对象与数组、枚举与组合
数据模型,说白了就是接口的「骨架」。你想想看,如果连数据长什么样都说不清楚,那前端后端怎么协作?我见过太多团队因为数据格式没对齐,联调时吵得不可开交。今天我们就来聊聊怎么把这个骨架搭结实。
4.1 Schema定义:给数据立规矩
Schema就是一份契约。它告诉调用方:你传什么、我返什么、每个字段是什么类型、哪些必填、哪些可选。没有Schema,接口文档就是一张废纸。
我个人习惯用JSON Schema来定义RESTful接口的数据模型。它跨语言、可校验、可生成文档,一套定义到处用。举个例子:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "用户注册请求",
"type": "object",
"required": ["username", "email", "password"],
"properties": {
"username": {
"type": "string",
"minLength": 3,
"maxLength": 20,
"pattern": "^[a-zA-Z0-9_]+$"
},
"email": {
"type": "string",
"format": "email"
},
"password": {
"type": "string",
"minLength": 8,
"maxLength": 128
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150
}
}
}
你看,每个字段的约束都写得清清楚楚。username只能包含字母数字和下划线,长度3到20位。email必须符合邮箱格式。这些约束在服务端校验一遍,客户端校验一遍,双重保险。
4.2 数据类型约束:别让脏数据进门
数据类型约束是接口安全的第一道防线。你想想,如果年龄字段传了个字符串"abc",系统不崩才怪。我见过最离谱的一次,有人把手机号传成了数组,结果下游系统直接OOM。
常见的约束类型包括:
| 约束类型 | 说明 | 示例 |
|---|---|---|
| 类型约束 | 指定字段的数据类型 | string, integer, number, boolean |
| 长度约束 | 限制字符串或数组的长度 | minLength, maxLength, minItems, maxItems |
| 范围约束 | 限制数值的大小范围 | minimum, maximum, exclusiveMinimum |
| 格式约束 | 校验特定格式 | email, uri, date-time, ipv4 |
| 正则约束 | 用正则表达式校验 | pattern: "^[0-9]{11}$" |
为什么要这么严格?因为接口一旦暴露出去,你根本不知道调用方会传什么鬼东西。我曾经遇到一个案例,客户端把用户ID传成了SQL注入语句,幸好服务端做了类型校验,直接返回400。要是没这层防护,后果不堪设想。
4.3 嵌套对象与数组:处理复杂结构
现实世界的数据很少是扁平的。一个订单包含多个商品,一个用户有多条地址,这些都是嵌套结构。怎么在Schema里表达?
嵌套对象用properties里再套properties。数组用items指定元素类型。看这个例子:
{
"type": "object",
"required": ["orderId", "items"],
"properties": {
"orderId": {
"type": "string",
"pattern": "^ORD\\d{10}$"
},
"items": {
"type": "array",
"minItems": 1,
"maxItems": 100,
"items": {
"type": "object",
"required": ["productId", "quantity", "price"],
"properties": {
"productId": { "type": "string" },
"quantity": {
"type": "integer",
"minimum": 1,
"maximum": 999
},
"price": {
"type": "number",
"minimum": 0.01
}
}
}
},
"shippingAddress": {
"type": "object",
"properties": {
"province": { "type": "string" },
"city": { "type": "string" },
"detail": { "type": "string" }
}
}
}
}
这里有几个要点:
- 数组要限制长度。我见过有人一次传10万条数据,直接把服务端内存打爆。
- 嵌套层级不要太深。超过3层,可读性就差了。我建议最多嵌套4层。
- 数组元素要定义清楚。别用
items: {}这种空对象,那等于没约束。
为什么会这样?因为嵌套结构一旦复杂,校验成本就上去了。你想想,一个订单有100个商品,每个商品又要校验productId、quantity、price,那就是300次校验。如果嵌套再深一层,校验次数指数级增长。
4.4 枚举与组合:限定可选范围
有些字段的值是固定的。比如订单状态:待支付、已支付、已发货、已完成、已取消。这种场景用枚举最合适。
{
"orderStatus": {
"type": "string",
"enum": ["PENDING", "PAID", "SHIPPED", "COMPLETED", "CANCELLED"]
},
"paymentMethod": {
"type": "string",
"enum": ["ALIPAY", "WECHAT", "CREDIT_CARD", "BANK_TRANSFER"]
}
}
枚举的好处是:
- 调用方不会传错值。传了"已付款"而不是"PAID",直接报错。
- 文档自动生成。枚举值就是接口文档的一部分。
- 服务端不用做if-else判断。校验器帮你挡掉了非法值。
组合呢?就是oneOf、anyOf、allOf这些。它们用来表达「或」的关系。比如一个支付请求,可能是支付宝支付,也可能是微信支付,两者结构不同:
{
"oneOf": [
{
"type": "object",
"required": ["alipayAccount", "alipaySign"],
"properties": {
"alipayAccount": { "type": "string" },
"alipaySign": { "type": "string" }
}
},
{
"type": "object",
"required": ["wechatOpenId", "wechatAppId"],
"properties": {
"wechatOpenId": { "type": "string" },
"wechatAppId": { "type": "string" }
}
}
]
}
这里用oneOf表示:请求必须符合其中一个结构,不能同时符合两个,也不能一个都不符合。我遇到过有人用anyOf,结果客户端传了支付宝和微信两套参数,服务端不知道该用哪个,直接报500。
oneOf:必须且只能匹配一个anyOf:至少匹配一个(可以多个)allOf:必须匹配所有
4.5 知识结构总览
下面这张图把数据模型的核心要素串起来了。你可以看到,从Schema定义出发,到数据类型约束、嵌套结构、枚举组合,每一步都在加固接口的可靠性。
4.6 序列化:从模型到字节流
数据模型定义好了,怎么在网络里传输?这就涉及到序列化。JSON是目前最主流的序列化格式,但也不是唯一选择。
| 序列化格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JSON | 可读性好、跨语言、生态丰富 | 体积大、无类型 | RESTful API、Web应用 |
| Protocol Buffers | 体积小、速度快、强类型 | 可读性差、需要定义.proto文件 | 微服务间通信、高性能场景 |
| MessagePack | 比JSON小、兼容JSON | 生态不如JSON | 移动端、IoT设备 |
| Avro | 支持Schema演化、压缩率高 | 学习成本高 | 大数据场景、Kafka消息 |
我个人偏好JSON+JSON Schema的组合。原因很简单:可读性好,调试方便。你想想,出问题的时候,抓个包就能看懂数据长什么样。要是用Protobuf,还得先反序列化,多一道工序。
但Protobuf在性能上确实有优势。我做过压测,同样的数据,Protobuf序列化后的体积只有JSON的1/3,序列化速度快了5倍。所以,内部服务间通信我建议用Protobuf,对外接口用JSON。
4.7 实战:一个完整的Schema示例
最后,我们来看一个完整的用户管理接口Schema。它综合了前面讲的所有知识点:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "用户管理接口",
"type": "object",
"required": ["action", "user"],
"properties": {
"action": {
"type": "string",
"enum": ["CREATE", "UPDATE", "DELETE", "QUERY"]
},
"user": {
"type": "object",
"required": ["username", "email"],
"properties": {
"username": {
"type": "string",
"minLength": 3,
"maxLength": 20,
"pattern": "^[a-zA-Z0-9_]+$"
},
"email": {
"type": "string",
"format": "email"
},
"roles": {
"type": "array",
"minItems": 1,
"maxItems": 5,
"items": {
"type": "string",
"enum": ["ADMIN", "EDITOR", "VIEWER", "GUEST"]
}
},
"profile": {
"type": "object",
"properties": {
"nickname": { "type": "string", "maxLength": 50 },
"avatar": { "type": "string", "format": "uri" },
"birthday": { "type": "string", "format": "date" }
}
}
}
},
"metadata": {
"type": "object",
"properties": {
"source": {
"type": "string",
"enum": ["WEB", "APP", "API"]
},
"timestamp": {
"type": "string",
"format": "date-time"
}
}
}
}
}
这个Schema定义了:
- 操作类型(枚举)
- 用户信息(嵌套对象)
- 角色列表(数组+枚举)
- 个人资料(嵌套对象)
- 元数据(嵌套对象+枚举+日期格式)
你看,所有知识点都用上了。这就是一个生产级别的Schema定义。嗯,照着这个写,基本不会出大问题。