JSON 的 10 个坑:文档不写,但上线后真会踩的那些
不是 "JSON 入门 30 分钟"。如果你已经在线上 ship 过 JSON API,那大概率被下面这些坑里的一两个搞过。整理一下,下次别再栽。
JSON 在 2017 年成为 RFC 8259 之前,已经被人用了十几年。每个语言、每个团队、每个序列化库对它的理解都微妙地不一样。下面这十个坑不是教程,是十年里我自己或者团队同学踩过的——记下来当备忘。
本文假设你已经会用 JSON.parse 和 json.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. undefined 和 NaN 序列化的诡异行为
JSON 标准里没有 undefined,也没有 NaN 和 Infinity。但各语言处理方式不一样:
| 值 | JavaScript | Python | Go |
|---|---|---|---|
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 格式化工具,它会同时给你格式化结果和错误行号。
常见原因按概率排序:
- 尾随逗号:
{"a": 1,}——人类觉得没问题,JSON 解析器拒绝 - 单引号:JSON 只认双引号
- 注释:
//或/* */都不合法 - 未转义的特殊字符(最常见的是字符串里的真换行符)
- 键名没加引号
10. 在 JSON 里嵌入二进制数据的正确姿势
JSON 是文本格式,二进制塞进去必须先编码。两种方案:
- Base64:兼容性最好,但体积膨胀 33%。用 Base64 工具 测试转换。
- Multipart 上传 + JSON 元数据分离:大文件场景的标准做法,HTTP 一个 multipart 请求里同时带 JSON 描述和二进制 part。
千万别试图把二进制直接塞进字符串字段——非 UTF-8 字节会让序列化在某些语言里直接崩溃。
上面 10 条里如果你只能记一条,就记第 1 条:跨语言大整数精度问题。其他都能搜出来,这个最容易在线上才暴露。
顺手能用的工具
本文里提到的格式化、Base64 编解码、文本对比都能在 DevUtils 直接打开用。不上传服务器,数据在浏览器里处理。