JSON 工具箱

接口返回的 JSON 该用什么结构

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

进阶约 4 分钟发布于 2026-10-30更新于 2026-10-30
三种接口响应结构及其取舍

结论:用 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 会直接报错。

相关工具

延伸阅读