JSON 在 REST API 中的应用

现代 Web 开发中,前后端之间几乎都在用 JSON 传递数据。REST API 的请求和响应,默认格式就是 JSON。本文讲清 JSON 在 API 开发中怎么用,以及几个只有真正对接过接口才会踩到的坑。

为什么 API 都选 JSON

在 JSON 之前,API 常用 XML,表单提交则用 application/x-www-form-urlencoded。JSON 之所以胜出,是因为它同时满足了三方需求:

格式 浏览器原生解析 能表达嵌套结构 传输体积 现状
JSON 是,JSON.parse 是 小 当前主流
XML 否,需要 DOMParser 是 明显更大 老系统、部分企业接口
Form 表单 否,需手动拆解 不能 最小 只适合扁平的少量字段

三点具体优势:

  1. 体积小:同样的数据,JSON 比 XML 短得多。XML 每个字段都要一对开闭标签,冗余很高。
  2. 易解析:几乎所有语言都原生支持 JSON 解析,浏览器里一句 JSON.parse 就能把字符串变成对象,不需要引入解析器。
  3. 贴近对象:JSON 的结构和编程语言里的对象/字典天然对应,读出来就能直接用,不用再映射成另一套模型。

表单编码做不到第 3 点——它只能表达扁平的键值对,嵌套对象得靠 user[profile][city] 这种约定俗成的写法,前后端容易对不上。

JSON:API 规范:一个容易混淆的同名标准

搜索「JSON API」时,你会遇到两个完全不同的东西,先分清它们能省很多时间:

如果你的团队需要一个跨端统一的接口契约,JSON:API 值得了解。它的核心规定是:

媒体类型必须是 application/vnd.api+json,不是普通的 application/json。服务端收到别的类型时应当返回 415 Unsupported Media Type。

文档顶层只允许这几个成员:

成员 作用
data 主数据,可以是一个资源对象或资源对象数组
errors 错误对象数组
meta 非标准的元信息,比如分页信息、统计数
jsonapi 描述服务端实现的规范版本等信息
links 与整个文档相关的链接
included 与主数据相关联的资源对象数组(用于侧载,避免 N+1 请求)

两条硬规则:data 和 errors 不能同时出现(一个响应要么成功要么失败);文档里没有 data 时,included 也不允许出现。

当前版本是 1.1,并且遵循「只增不删」的兼容策略——新版本不会移除已有成员,所以按 1.1 写的客户端不会因为规范升级而失效。

什么时候不该用它:内部几个简单 CRUD 接口、只有你一个人对接的服务,套上这套规范属于过度设计。它的价值在多方协作、需要长期演进的大型 API 上才体现得出来。

请求体(Request Body)

前端向后端提交数据时,Content-Type 必须设为 application/json,请求体就是 JSON:

POST /api/users
Content-Type: application/json

{
  "name": "Tom",
  "age": 30,
  "email": "tom@example.com"
}

Content-Type 漏写是个高频事故:很多后端框架看到没有这个头,会退回去按表单格式解析,结果你在 JSON 里写的嵌套结构全变成了空值,而后端日志里看不出任何异常,只看到字段是 undefined。

关于 charset:JSON 规范(RFC 8259)已经规定 JSON 文本必须用 UTF-8 编码,所以 application/json; charset=utf-8 里的 charset 是冗余的。写上无害,但不要指望它能解决中文乱码——乱码通常是后端或数据库的编码问题。

不同 HTTP 方法带 JSON 的习惯不一样:

方法 语义 是否带请求体 带什么
GET 查询 通常不带 参数放 URL query
POST 创建 带 完整的新对象
PUT 整体替换 带 完整对象(没传的字段会被清空)
PATCH 部分更新 带 只带要改的字段
DELETE 删除 通常不带 一般不需要额外数据

GET 虽然 HTTP 协议允许带 body,但实际上很多网关、代理、以及部分 HTTP 客户端库会直接丢掉它。不要在 GET 里放请求体,这是实践中反复踩的坑。

PUT 和 PATCH 的区别经常被搞混:PUT 是「用我给的这一份完整替换掉旧的」,PATCH 是「只改我给的这几个字段」。用 PUT 只传一个字段,其他字段可能被置空。

响应体(Response Body)

后端返回数据时,同样用 JSON:

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 1,
    "name": "Tom",
    "tags": ["前端", "后端"]
  }
}

这种「状态码 + 消息 + 数据」的三段式结构在国内非常常见,好处是前端可以统一拦截处理。但它有个隐患:业务码和 HTTP 状态码是两套并行的机制,很容易出现「HTTP 200 但业务其实是失败的」这种不一致,前端稍不注意就以为成功了。

更规范的做法是让 HTTP 状态码本身承担语义,错误响应遵循 RFC 7807(Problem Details for HTTP APIs),媒体类型用 application/problem+json:

{
  "type": "https://example.com/errors/out-of-credit",
  "title": "余额不足",
  "status": 403,
  "detail": "当前余额 30 元,本次操作需要 50 元",
  "instance": "/accounts/12345/transactions/abc"
}
字段 含义
type 错误类型的文档链接,人能点进去看说明
title 简短的错误摘要,同类错误应该一致
status 与 HTTP 状态码相同的数字,方便日志排查
detail 本次错误的具体说明,可以带上下文数据
instance 出问题的那个具体请求的标识

它的好处是标准化——任何懂这个规范的前端或第三方都能直接读懂你的错误结构,不需要你写文档解释。

分页怎么用 JSON 表达

列表接口几乎都要分页。两种主流结构,选错了后期很难改:

偏移量分页(offset / limit)

{
  "data": [ { "id": 1 }, { "id": 2 } ],
  "page": {
    "offset": 0,
    "limit": 20,
    "total": 348
  }
}

优点:能直接跳到第 N 页,前端好做页码器。

缺点有两个,在数据量大时会很明显:total 需要额外执行一次 COUNT(*),百万级表上这个查询可能比取数据本身还慢;另外数据在翻页过程中有增删时,会出现重复项或漏项——你按 offset=20 取第二页,但如果前面刚删了一条,原本第 21 条就变成了第 20 条,被跳过。

游标分页(cursor)

{
  "data": [ { "id": 101 }, { "id": 102 } ],
  "page": {
    "next_cursor": "eyJpZCI6MTAyfQ",
    "has_more": true
  }
}

游标一般是对「最后一条记录的位置」做的编码(比如最后一条的 id 或时间戳)。优点是性能稳定(不需要 count,也不受 offset 增大而变慢)、数据变动时不会重复或遗漏。

缺点是不能跳页,只能「下一页」。所以它适合信息流、日志、无限滚动这类场景。

怎么选:后台管理列表、需要页码跳转的,用 offset;面向用户的信息流、数据量大且会频繁变动的,用 cursor。如果一开始不确定,优先 cursor——它后期改成 offset 容易,反过来很难。

大数字精度丢失:订单号为什么会变

这是后端和前端吵得最多的问题之一。先看现象:

JSON.parse('{"id": 1234567890123456789}').id
// 实际得到:1234567890123456800

末尾几位变了。 而且没有任何报错,静悄悄就错了。

原因:JSON 规范没有为整数单独定义类型,所有数字都按 IEEE 754 双精度浮点数处理。这个格式能精确表示的整数上限是 2 的 53 次方减一,也就是 9007199254740991(16 位)。

超过这个范围,数字会被四舍五入到最近的能表示的值。19 位的雪花 ID、某些支付平台的交易号,正好都在这个范围之外。

正确的解决方式是让后端把 ID 序列化成字符串:

{
  "id": "1234567890123456789"
}

加上引号之后,前端拿到的是字符串,一丝不差。前端不要自作主张 parseInt 或 Number() 转回数字,否则又白费了。

如果后端改不了(比如是第三方接口),前端只能在 JSON.parse 之前做预处理,把长数字字段先转成字符串,或者用支持大数的解析库。这类方案都有成本,所以能在后端解决就在后端解决。

同样的问题也会出现在金额上——用浮点数存 0.1 + 0.2 得不到 0.3。金额通常建议用「分」为单位的整数,或者直接用字符串。

API 返回的 JSON 长什么样

实际接口返回的 JSON 往往层级很深,比如:

{
  "data": {
    "list": [
      { "id": 1, "user": { "name": "Tom", "profile": { "city": "北京" } } },
      { "id": 2, "user": { "name": "Jerry", "profile": { "city": "上海" } } }
    ],
    "total": 2
  }
}

这种数据直接看文本很费劲,推荐用 JSON 在线格式化工具 的三个视图处理:

开发 API 时的 JSON 常见坑

除了上面单独讲的精度问题,还有几个反复出现的:

遇到「接口返回解析失败」,第一步先确认返回的 JSON 是否合法——把返回内容粘到 JSON 在线校验工具 里检查,能秒级定位是格式问题还是字段问题。

延伸阅读

常见问题

为什么 API 都用 JSON 而不用 XML?

JSON 体积更小传输快、几乎所有语言原生支持解析、结构和编程语言的对象天然对应,所以取代了 XML 成为主流。

REST API 的响应体结构通常是什么样?

常见的是「状态码 + 消息 + 数据」三段式结构,例如 code、message、data,data 里放真正的业务数据。更规范的做法是遵循 RFC 7807 的 Problem Details 格式返回错误。

接口返回的 JSON 解析失败怎么办?

第一步确认返回的 JSON 是否合法,把返回内容粘贴到在线校验工具里检查,能秒级定位是格式问题还是字段问题。如果错误信息是 Unexpected end of JSON input,通常是响应体为空或被截断。

JSON:API 和「用 JSON 的 API」是一回事吗?

不是。JSON:API 是一个有正式规范的 API 设计标准,媒体类型是 application/vnd.api+json,规定了文档的顶层结构。而「用 JSON 的 API」只是说接口用 JSON 传数据,结构完全自由。

接口返回的大数字 ID 为什么末几位会变?

JSON 的数字按 IEEE 754 双精度浮点存储,能精确表示的最大整数是 2 的 53 次方减一,即 9007199254740991。超过这个范围的 ID(比如 19 位雪花 ID)会丢失精度,必须让后端用字符串返回。