后端 · 数据格式

JSON 的 10 个坑:文档不写,但上线后真会踩的那些

不是 "JSON 入门 30 分钟"。如果你已经在线上 ship 过 JSON API,那大概率被下面这些坑里的一两个搞过。整理一下,下次别再栽。

JSON 在 2017 年成为 RFC 8259 之前,已经被人用了十几年。每个语言、每个团队、每个序列化库对它的理解都微妙地不一样。下面这十个坑不是教程,是十年里我自己或者团队同学踩过的——记下来当备忘。

📌

本文假设你已经会用 JSON.parsejson.Marshal 这种基本操作。如果还在找格式化工具,直接去 JSON 格式化

1. JavaScript 的大整数精度,是默认坑

后端 Java/Go 用 int64 存 ID 很常见,序列化进 JSON 就是一个 19 位的数字。前端 JSON.parse 拿到之后——数字最后几位会变。

JSON.parse('{"id":9007199254740993}').id
// 9007199254740992    ← 最后那个 3 没了

原因是 JS 的 Number 内部是 64 位浮点,安全整数上限是 253 - 1。超出之后精度悄无声息地消失。

修复办法是后端把大 ID 序列化成字符串。Jackson 上 @JsonSerialize(using = ToStringSerializer.class),Go 上 json:"id,string"。或者前端用 json-bigint 替换原生解析。

⚠️

真实事故:订单系统 ID 超过 253 后,前端把两个不同订单识别成同一个,下单按钮一直转。排查了半天才意识到是数字精度问题。

2. undefinedNaN 序列化的诡异行为

JSON 标准里没有 undefined,也没有 NaNInfinity。但各语言处理方式不一样:

JavaScriptPythonGo
undefined整个字段被丢弃
NaN序列化为 null序列化为 NaN(非法 JSON)报错
数组中的 undefined替换成 null
JSON.stringify({a: undefined, b: NaN, c: [1, undefined, 3]})
// '{"b":null,"c":[1,null,3]}'

Python 的 json.dumps 默认会输出 NaN,但其他 JSON 解析器(比如 Java、浏览器)不接受。要么传前过滤掉,要么 json.dumps(data, allow_nan=False) 让它直接报错暴露问题。

3. 浮点数会让你怀疑人生

金额、汇率、坐标——任何需要精度的场景都别用 JSON 的 number 类型存浮点。

JSON.parse('{"price": 0.1 + 0.2}')  // 这是不合法的 JSON
JSON.stringify({price: 0.1 + 0.2}) // '{"price":0.30000000000000004}'

对金额场景,约定俗成是用字符串传("price": "9.99")或者用最小单位存整数("price_cents": 999)。后者跨语言友好得多。

4. 循环引用:JSON.stringify 直接崩

React 的 fiber 节点、Vue 的响应式对象、ORM 实体——任何能往父对象反向引用的结构,序列化都会爆。

const a = {name: "a"};
a.self = a;
JSON.stringify(a);
// Uncaught TypeError: Converting circular structure to JSON

常见的处理是用 flatted 或者自己写 replacer:

function safeStringify(obj) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (k, v) => {
    if (typeof v === "object" && v !== null) {
      if (seen.has(v)) return "[Circular]";
      seen.add(v);
    }
    return v;
  });
}

5. jq 比你想的强多了

大多数人用 jq 就是 cat foo.json | jq . 美化输出,但它能做的事远不止这些。

# 提取嵌套字段,注意 -r 让字符串去引号
cat resp.json | jq -r '.data.users[].email'

# 过滤 + 投影
cat orders.json | jq '[.[] | select(.status == "paid") | {id, amount}]'

# 把数组转 CSV(写脚本前先试试)
cat data.json | jq -r '.[] | [.name, .age, .city] | @csv'

# 合并多个 JSON 文件
jq -s 'add' file1.json file2.json

# 修改字段然后写回
jq '.version = "2.0.0"' package.json | sponge package.json

把这些贴在 jqplay.org 上随便试,比读文档快得多。

6. JSON Schema 不是验证,是契约

很多团队把 Schema 当成请求参数校验工具——参数错了返回 400。但它真正的价值在跨语言共享数据契约。一份 Schema 文件,Java 后端用 jackson-module-jsonSchema 校验,前端用 ajv 校验,再加一个工具生成 TypeScript 类型——前后端再也不会因为字段名拼错吵架。

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["name", "age"],
  "properties": {
    "name": {"type": "string", "minLength": 1, "maxLength": 50},
    "age":  {"type": "integer", "minimum": 0, "maximum": 150},
    "email":{"type": "string", "format": "email"}
  },
  "additionalProperties": false
}

关键是最后那行 additionalProperties: false。默认值是 true,意味着多传一个字段也算合法——上线后用户传了脏数据,业务侧无感。

7. 流式解析超大 JSON

有些场景拿到的是几百 MB 的 JSON Lines 或者一个超大 JSON 数组。JSON.parse 一次性 load 进内存——OOM。

Node 上用 stream-json,Python 上用 ijson,Go 上用 json.Decoder 的 token 模式。共同思路:边读边解析、按需 yield。

8. 永远不要用 eval()

提到这个不是因为有人真这么干,是因为有些"老代码"或者 jsonp callback 处理里会偷懒用 eval。任意 JS 注入风险,2026 年了别再写了。

// 坏:
const data = eval("(" + str + ")");

// 好:
let data;
try { data = JSON.parse(str); }
catch (e) { /* 处理错误 */ }

9. 排查"Unexpected token"的笨办法

JSON 报错从来不告诉你具体哪一行。错误信息只给一个字符位置。最快的定位方式:复制到 JSON 格式化工具,它会同时给你格式化结果和错误行号。

常见原因按概率排序:

  1. 尾随逗号:{"a": 1,}——人类觉得没问题,JSON 解析器拒绝
  2. 单引号:JSON 只认双引号
  3. 注释:///* */ 都不合法
  4. 未转义的特殊字符(最常见的是字符串里的真换行符)
  5. 键名没加引号

10. 在 JSON 里嵌入二进制数据的正确姿势

JSON 是文本格式,二进制塞进去必须先编码。两种方案:

千万别试图把二进制直接塞进字符串字段——非 UTF-8 字节会让序列化在某些语言里直接崩溃。

💡

上面 10 条里如果你只能记一条,就记第 1 条:跨语言大整数精度问题。其他都能搜出来,这个最容易在线上才暴露。

顺手能用的工具

本文里提到的格式化、Base64 编解码、文本对比都能在 DevUtils 直接打开用。不上传服务器,数据在浏览器里处理。

JSON 格式化 · 校验 · 压缩

粘贴即用,自动定位语法错误,支持大文件

打开工具