4. 数据格式规范:JSON Schema 定义、字段约束、版本控制策略
做接口对接,最怕什么?我最怕两边对字段解释不一样。你发个 status 字段,他以为 0 是成功,你以为是 1。这种问题,在 DLP 外发审计场景里特别要命——审计日志一旦格式乱了,整个闭环就堵死了。
所以这一章,我们专门聊聊数据格式规范。说白了,就是用一套严格的标准把接口的“长相”定死。我个人经验是:前期格式规范做得越细,联调时吵的架就越少。
4.1 为什么需要数据格式规范?
先看一个真实场景。DLP 系统外发告警时,会把事件序列化成 JSON 推给第三方。有一次,我们的开发同事忘了加 severity 字段,第三方直接报错。为什么?因为他们那边的 JSON Schema 校验没通过,整条消息被丢了。
你看,没规范就像没红绿灯。每个人按自己理解开车,不出事故才怪。
4.2 JSON Schema 定义
JSON Schema 是什么?它就是一个 JSON 文件,用来描述另一个 JSON 长什么样。有点像我们填表格时的“填表说明”。
我习惯先定义 Schema,再写代码。这样前端后端都看着同一个标准,谁也跑不了。
4.2.1 基础结构
一个简单的例子:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://example.com/audit-event.schema.json",
"title": "审计事件",
"type": "object",
"properties": {
"eventId": {
"type": "string",
"description": "全局唯一事件 ID"
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "事件发生时间(ISO 8601)"
},
"severity": {
"type": "integer",
"minimum": 0,
"maximum": 5,
"description": "告警严重等级"
}
},
"required": ["eventId", "timestamp", "severity"]
}
注意看 $id 字段。我建议把它设成一个可访问的 URL,方便团队直接打开浏览器查看。万一有人改 Schema,也能追溯。
schemas/ 目录下,版本控制走 Git。这样每次修改都有记录,出问题能回退。
4.2.2 引用与复用
当字段太多时,可以拆成子 Schema,用 $ref 引用。比如把“用户信息”单独定义:
{
"definitions": {
"user": {
"type": "object",
"properties": {
"userId": { "type": "string" },
"userName": { "type": "string" }
},
"required": ["userId"]
}
},
"properties": {
"sender": { "$ref": "#/definitions/user" },
"receiver": { "$ref": "#/definitions/user" }
}
}
这样做的好处是,修改只需一处,所有引用自动生效。我经历过一个项目,因为没复用,改了五六个副本,漏了一个,测试才暴露——惨痛教训。
4.3 字段约束
光定义类型还不够。你想想看,"severity": 6 这种值应该被拒绝,因为它超出了 0~5 的范围。字段约束,就是给数据加一把锁。
4.3.1 常用约束类型
| 约束类型 | 关键词 | 示例 | 说明 |
|---|---|---|---|
| 类型 | type | "type": "integer" | string / number / integer / boolean / array / object / null |
| 最小/最大值 | minimum, maximum | "minimum": 0, "maximum": 5 | 适用于 number / integer |
| 字符串长度 | minLength, maxLength | "minLength": 1, "maxLength": 128 | 防止恶意超长字符串 |
| 正则模式 | pattern | "pattern": "^[a-zA-Z0-9_]+$" | 限制字符集,比如用户ID |
| 枚举 | enum | "enum": ["low", "medium", "high"] | 只能取给定值之一 |
| 格式 | format | "format": "email" | date-time / email / uri / ipv4 等 |
| 数组长度 | minItems, maxItems | "minItems": 1, "maxItems": 100 | 限制数组大小 |
| 额外属性 | additionalProperties | "additionalProperties": false | 禁止出现未定义字段 |
这里我重点说一下 additionalProperties。我曾经在对接第三方时,他们悄悄加了一个 secret 字段,结果我们解析时没处理,程序直接崩溃。从那以后,我所有公开接口的 Schema 都加上 "additionalProperties": false——谁也别想偷偷塞东西。
4.3.2 业务约束示例
审计事件里有个 action 字段,表示操作类型。我们规定只能取 create、read、update、delete 四个值。用枚举约束:
"action": {
"type": "string",
"enum": ["create", "read", "update", "delete"]
}
再比如 fileSize,单位是字节,不能为负数:
"fileSize": {
"type": "integer",
"minimum": 0,
"maximum": 1073741824
}
为什么上限是 1GB?因为我们的 DLP 系统对超过 1GB 的文件会单独处理,不走标准外发通道。这个约束是跟业务挂钩的,不是拍脑袋定的。
4.4 版本控制策略
接口会变,Schema 也会变。问题来了:改了 Schema 后,旧版调用方怎么办?
我见过最粗暴的做法:直接通知对方“我们接口更新了,你们快点改”。结果对方说“我们排期到三个月后了”。两边都不爽。
所以版本控制,解决的核心是“兼容”与“平滑演进”。
4.4.1 版本号怎么放?
两种主流方式:
- URL 路径版本:
/api/v1/audit、/api/v2/audit。简单直观,但 URL 会变长。 - 请求头版本:在
Accept头里指定版本,比如Accept: application/vnd.company.audit+json; version=2。更 RESTful,但客户端实现稍复杂。
我个人偏向 URL 路径版本。为什么?因为浏览器里直接敲 URL 就能测试,对调试友好。但如果你做的是纯内部微服务,可以用请求头版本,省得 URL 结构复杂。
4.4.2 兼容性规则
改 Schema 时,要遵循“向前兼容”原则。简单说:老客户端用老 Schema 发送的请求,新服务必须能正确处理。
| 改动类型 | 是否兼容 | 举例 |
|---|---|---|
| 增加可选字段 | ✅ 兼容 | 新增 description 字段,不是 required |
| 增加必填字段 | ❌ 不兼容 | 老数据没有这个字段,会校验失败 |
| 缩小枚举值范围 | ❌ 不兼容 | 原来 enum: [a,b,c] 变成 [a,b],老客户端发的 c 被拒绝 |
| 扩大枚举值范围 | ✅ 兼容 | 新增一个值,老客户端没发它,没问题 |
| 放宽约束(降低最小值) | ✅ 兼容 | minimum 从 0 改成 -10,老数据 0 仍合法 |
| 收紧约束(提高最小值) | ❌ 不兼容 | 老数据可能低于新最小值 |
| 删除字段 | ❌ 不兼容 | 老客户端还在发这个字段,新服务可能不认识而忽略,但建议保留 |
userId 改成 userAccount,结果下游系统三天后才发现。版本升级必须走正式变更流程。
4.4.3 多版本共存策略
如果实在要做不兼容变更,建议同时发布两个版本,给调用方一段迁移时间。比如:
/api/v1/audit保持老 Schema/api/v2/audit使用新 Schema
然后在 Router 层加一个版本分发。我习惯用 Nginx 或网关做:根据请求路径前缀转发到不同的微服务实例。这样旧版服务可以单独维护,直到所有调用方都迁移完再下线。
Warning: deprecation 响应头提醒调用方。
4.5 知识结构图
下面我把这节的知识点串成一张图。你可以用它当大纲,写 Schema 时对照着检查。
4.6 落地建议
最后,我把这章的核心动作总结成三步:
- 编写 JSON Schema:用
$schema指定草案版本,用$id标识唯一地址。所有字段都配上description,给后来人看。 - 施加字段约束:能加
minimum就加,能加enum就加,additionalProperties默认设成false。别怕约束多,怕的是约束不够。 - 规划版本策略:选一种版本号位置,定好兼容性规则。不兼容改动时必须给迁移窗口,别搞“突然死亡”。