接口返回的 JSON 该用什么结构
结论:三种常见结构各有代价:裸返回最简洁但扩展时要破坏兼容;固定信封安全但冗余;JSON:API 规范完整但重。中小型项目最实用的是「HTTP 状态码表结果 + 固定信封包业务数据」。

结论:用 4xx/5xx 状态码表达成败,用 { "data": ..., "meta": ... } 承载业务数据,错误时换成 error 信封;内部项目做到这一步就够,只有对外开放平台才需要再引入 JSON:API 或 OpenAPI。
先排除最差的一种:永远返回 200,把成败写在 code 字段里。
三种主流结构
A. 裸返回
{ "id": 1, "name": "demo", "price": 99 }
最简洁,HTTP 语义完整(404 就是 404)。问题在于想加元信息(分页、警告、调试信息)时没有位置,只能临时加字段,破坏原有契约。
B. 固定信封
{
"data": { "id": 1, "name": "demo" },
"meta": { "requestId": "abc", "took": 12 }
}
业务数据与元信息分离,加字段不破坏 data 的形状。代价是多一层嵌套,前端每次都要 .data。
C. 全信息信封(不推荐)
{ "code": 0, "message": "success", "data": {...} }
问题不在结构本身,而在配套做法:HTTP 状态码永远 200,错误码自定义。后果是 CDN、网关、熔断、重试、监控全部看不懂真实成败,所有告警要自己写解析逻辑。
推荐组合
用 HTTP 状态码表达结果,用信封承载业务数据。
成功:
{
"data": { "id": 1, "name": "demo" },
"meta": { "requestId": "req_8f2a", "took": 12 }
}
失败:
{
"error": {
"code": "ORDER_NOT_FOUND",
"message": "订单不存在或已被删除",
"details": [{ "field": "id", "issue": "not_found" }]
},
"meta": { "requestId": "req_8f2a" }
}
状态码用 4xx/5xx,code 用机器可读的稳定标识(不是给人看的中文),message 给人看,details 放字段级错误供表单回填。
几条硬规则
1. 列表一定要有分页元信息。
{
"data": [{ "id": 1 }],
"meta": { "total": 132, "page": 1, "size": 20, "hasNext": true }
}
没有 total 或游标,前端就没法做分页控件。
2. 空列表返回 [],不要返回 null。 前端 .map 直接崩是常见事故。
3. 时间统一 ISO 8601 带时区。 详见 时间字段用什么格式。
4. 字段命名全站统一。 snake_case 还是 camelCase 选一个,别一个接口一种。跨语言时注意:Go、Python 偏好 snake_case,JS 生态偏好 camelCase,转换放在边界层做一次,别让前端到处适配。
5. 金额用整数最小单位或字符串。 99.99 在浮点里不精确,9999(分)或 "99.99" 都行,但要在文档里写死。
6. 别把 null 和「字段不存在」混用。 两者语义不同,见 空值用 null 还是省略字段。
什么时候上规范
- 对外开放平台、多方对接:考虑 JSON:API 或 OpenAPI 描述 + 统一信封
- 内部前后端一体:固定信封足够,别过度设计
- 需要强类型契约:OpenAPI/JSON Schema 先行,结构从 Schema 生成
规范的价值在于「不用每次讨论」,成本在于「每个新人都要学」。团队五个人的项目上 JSON:API,多数时候是负收益。
常见问题
裸返回和固定信封怎么选?
要分页、警告、requestId 这类元信息时用固定信封,让 meta 与 data 分开增长。完全不需要元信息、且能接受以后加字段会破坏契约时,才用裸返回。
为什么不该永远返回 200?
CDN、网关、熔断、重试和监控都只看 HTTP 状态码来决定是否缓存、重试或告警。状态码永远是 200 时,它们全都把失败当成成功,每个告警还要自己解析业务码。
列表接口是否必须有分页元信息?
是。没有 total 或游标,前端就做不出分页控件;同时空列表要返回 [] 而不是 null,否则前端的 .map 会直接报错。
相关工具
- JSON 格式化与校验:调接口时对照原始响应体
- JSON API 数据契约设计要点:字段设计、版本兼容与错误码约定