团队协作常见问题:契约、配置与夹具
结论:命名风格、时间格式、空值语义、枚举形态、配置格式、夹具组织与合并冲突,十二条团队里真正会吵起来的取舍。

结论:一次性把这几条写进团队约定——返回体用 {"data":…,"meta":…} 加 HTTP 状态码、命名全站统一一种风格、时间用带时区的 RFC 3339 字符串、枚举用字符串;之后所有新接口都按同一份契约「只加不改不删」地演进。
这些问题没有唯一正确答案,但必须有唯一团队答案——否则每次新接口都要重新吵一遍。
一、接口契约
接口返回的 JSON 该用什么结构?
三种常见形态,各有代价:
| 形态 | 例子 | 优点 | 缺点 |
|---|---|---|---|
| 裸数据 | [{...}, {...}] |
简单 | 无法扩展元信息 |
| 固定包装 | {"code":0,"message":"ok","data":{...}} |
统一错误处理 | 业务码与 HTTP 状态重复 |
| 资源包装 | {"data":{...},"meta":{...},"links":{...}} |
可扩展、语义清晰 | 层级多一层 |
对内的业务 API 推荐「资源包装 + HTTP 状态码表达结果」:
{
"data": [{ "id": "1", "name": "示例" }],
"meta": { "total": 128, "page": 2, "pageSize": 20 }
}
错误时用 HTTP 状态码 + 结构化错误体,不要再套一层 code:
{
"error": {
"type": "validation_failed",
"message": "请求参数校验失败",
"details": [
{ "path": "/email", "reason": "格式不正确" }
]
}
}
关键点:HTTP 状态码已经表达了成功或失败,业务码是冗余的。两套体系并存时, 「201 但 code 是 500」这种不一致几乎必然出现。
裸数组要避免。顶层是数组时无法附加分页、总数、游标,将来想加就得改所有调用方。
字段命名用 snake_case 还是 camelCase?
选一个,全站统一,不要在同一份 JSON 里混用。
| 风格 | 常见生态 | 例子 |
|---|---|---|
camelCase |
JavaScript / TypeScript / Java | userName, createdAt |
snake_case |
Python / Ruby / Go(部分) / 数据库列名 | user_name, created_at |
kebab-case |
罕见,键名里带连字符访问不便 | user-name |
判断依据:
- 谁消费这份 JSON:前端为主选 camelCase,数据管道与 Python 服务为主选 snake_case;
- 是否直接映射数据库列:直接透传表结构时用 snake_case 能省一层转换;
- 存量系统延续:已有接口是什么就用什么,一致性比正确性重要。
真要转换,放在边界做(网关或前端请求层),不要在业务代码里散落 toCamelCase 调用。
时间字段用什么格式?
用 RFC 3339 / ISO 8601 字符串,带时区偏移:
{
"createdAt": "2026-10-30T11:37:57+08:00",
"updatedAt": "2026-10-30T03:37:57Z"
}
要点:
- 带时区:不带时区的
2026-10-30 11:37:57在跨时区场景下必然出问题; - 统一
Z或带偏移:两者都对,但一个接口里别混用; - 不用时间戳数字:秒还是毫秒、是否含闰秒,每个团队都要重新解释一遍。需要紧凑时用毫秒整数, 但要在文档里写死单位。
字段名建议用 createdAt / updatedAt / deletedAt 这类 At 后缀,
一眼能看出是时间点而不是时长。时长用 timeoutMs、ttlSeconds,单位写进名字。
空值用 null 还是省略字段?
取决于「这个字段有没有意义」:
| 语义 | 写法 | 说明 |
|---|---|---|
| 字段存在但暂无值 | "name": null |
明确表达「查过了,就是空」 |
| 字段与该资源无关 | 省略 | 例如普通用户不该有 vipExpireAt |
| 部分更新(PATCH) | 省略 = 不改 | 这时 null 才表示「清空」 |
第三种最容易被忽略。在 PATCH 请求里:
{ "nickname": null }
意思是「把昵称清空」,而请求体里不出现 nickname 表示「不要动昵称」。
如果两者都当「清空」处理,用户改个邮箱就会把昵称清掉。
再强调一次:不要用 undefined。它不是合法 JSON 值,JSON.stringify 会直接丢掉这个键,
行为与「省略」相同,但代码里看起来像「显式传了空」。
布尔字段怎么命名?
用 is / has / can / should 前缀,避免用否定式。
{
"isActive": true,
"hasPassword": false,
"canEdit": true
}
三条经验:
- 不要否定式:
isDisabled: false需要读者做一次取反,isEnabled: true更直接。 双重否定(isNotDisabled)是灾难; - 不要用数字表示布尔:
"enabled": 1会让每个消费方都写一次=== 1; - 不要用字符串:
"enable": "true"在 JavaScript 里是 truthy,if (data.enable)对"false"也成立,这类 bug 极难排查。
枚举字段用数字还是字符串?
用字符串,除非有明确的存储或带宽压力。
{ "status": "active" }
对比 { "status": 1 },字符串的优势:
- 日志、抓包、数据库里直接可读,不用查映射表;
- 新增枚举值不影响已有值的含义(数字枚举中间插入一个值就会错位);
- 前端不需要同步维护一份
1 → 'active'的映射。
代价是多几个字节,压缩后几乎可忽略。
配套做法:字符串枚举值用小写 + 下划线(pending_review),并在 Schema 里用 enum 限定,
让非法值在校验阶段就被拦住。
二、配置与夹具
配置用 JSON 还是 YAML / TOML?
按用途选,不要按喜好选:
| 格式 | 适合 | 不适合 |
|---|---|---|
| JSON | 程序读写、需要严格结构、要过 Schema 校验 | 人工频繁编辑(不能写注释) |
| YAML | 多层嵌套的部署配置(K8s) | 手写容易缩进出错;yes/no 会被转成布尔 |
| TOML | 扁平的依赖与工具配置(Cargo.toml、pyproject.toml) |
深层嵌套可读性差 |
| JSONC | 需要注释的编辑器配置(tsconfig.json) |
跨系统传输 |
团队里常见的错误是用 YAML 存业务配置——YAML 的隐式类型转换(1.0 变数字、
NO 变布尔、2026-10-30 变日期)会在不同解析器上产生不同结果。
如果配置要给程序做严格校验,选 JSON + Schema,注释用 _comment 字段或独立文档补充。
测试夹具怎么组织?
按「一份基准 + 最小差异」组织,不要每个用例复制一整份巨大 JSON。
fixtures/
├─ users/
│ ├─ base.json # 基准用户对象
│ ├─ no-email.json # 只有 email 缺失
│ └─ invalid-age.json # 只有 age 越界
└─ fixtures.ts # 加载与浅合并的工具函数
import baseUser from './users/base.json';
export function makeUser(overrides: Partial<User> = {}): User {
return { ...baseUser, ...overrides };
}
这样新增用例时只需要写差异字段,改基准字段时所有用例自动跟上。
三条纪律:
- 夹具要能被 Schema 校验:基准夹具本身就应该是合法数据;
- 不要从生产库导出真实数据:手机号、身份证、地址都是个人信息;
- 夹具进版本库,但要小:单文件超过几十 KB 就该考虑裁剪或生成。
JSON 文件合并冲突怎么减少?
JSON 天然容易冲突:格式化工具一次重排整个文件,两个人各改一行就会全文件冲突。
四个手段:
- 统一格式化并加进
.editorconfig:缩进、换行、末尾换行一致,避免无关 diff; - 排序键名:约定按字母序排列键,新增字段不会和别人的插入位置打架;
- 拆分文件:一个巨型
config.json拆成按领域分的多个文件,冲突面自然变小; - 避免「一键重排」提交:格式化与业务改动分成两个提交,review 时才看得清。
如果团队用锁文件(pnpm-lock.yaml、package-lock.json),冲突时不要手工合并,
重新生成即可:
git checkout --theirs pnpm-lock.yaml
pnpm install
怎么做版本兼容?
原则是「只加不改不删」。
| 变更 | 兼容性 | 做法 |
|---|---|---|
| 新增可选字段 | 兼容 | 直接加,消费方忽略未知字段 |
| 新增必填字段 | 不兼容 | 升版本,或给默认值 |
| 删除字段 | 不兼容 | 先标记废弃,观察调用方后再删 |
| 改字段类型 | 不兼容 | 新增字段名,双写过渡 |
| 改枚举值集合 | 视情况 | 新增值兼容,删值不兼容 |
配套机制:
- 消费方用「宽松解析」:忽略未知字段,字段缺失时给默认值;
- 服务端保留旧字段一段时间,并在响应里返回
Deprecation头提示; - Schema 里对新增字段不设
required,对废弃字段标deprecated: true。
怎么在 CI 里校验契约?
把 Schema 当成代码的一部分,提交时校验。
# 校验示例数据是否符合 Schema(用 ajv-cli)
npx ajv-cli validate -s schemas/user.json -d "fixtures/users/*.json" --strict=false
CI 里跑三件事:
- Schema 自身的合法性:
$schema声明是否有效、$ref能否解析; - 夹具是否符合 Schema:保证测试数据不会漂移;
- 契约是否向后兼容:对比上一个版本,检查是否删了字段、改了类型。
第三项需要工具支持(如 openapi-diff、json-schema-diff),
没有的话至少用脚本对比 required 数组与 properties 键集合的变化。
三、速查
| 问题 | 建议 |
|---|---|
| 返回结构 | {"data":..., "meta":...},错误用 HTTP 状态码 |
| 命名风格 | camelCase(前端为主)或 snake_case(数据为主),全站统一 |
| 时间格式 | RFC 3339 字符串,带时区,字段名以 At 结尾 |
| 空值 | 无值用 null,无关字段省略,PATCH 里省略表示不改 |
| 布尔命名 | is/has/can 前缀,不用否定式,不用 0/1 |
| 枚举 | 字符串,小写下划线,Schema 里加 enum |
| 配置格式 | 程序校验用 JSON,人工编辑用 TOML 或 JSONC |
| 夹具 | 一份基准 + 最小差异覆盖,能被 Schema 校验 |
| 合并冲突 | 统一格式化、排序键名、拆分文件 |
| 版本兼容 | 只加不改不删,废弃字段先标记再删 |