JSON 工具箱

语法与格式常见问题:注释、尾逗号与精度

结论:注释能不能写、尾逗号为什么报错、键名要不要加引号、9007199254740993 为什么变样,十二条结论全部对照 RFC 8259 给出。

入门约 12 分钟发布于 2026-10-30更新于 2026-10-30
语法与格式常见问题封面

结论:跨进程传输一律写标准 JSON——双引号、无注释、无尾逗号、UTF-8 不带 BOM;需要注释的配置改用 JSONC 或 JSON5,并在运行时转回标准 JSON;超过 2^53-1 的整数在服务端就序列化成字符串。

这些问题的共同点是:看起来「应该可以」,规范里其实写得很死。

一、什么算合法 JSON

JSON 和 JSON5、JSONC 有什么区别?

标准 JSON 只认双引号字符串,不允许注释与尾逗号。JSON5 与 JSONC 都是在它之上做加法:

特性 JSON (RFC 8259) JSONC JSON5
双引号字符串 支持 支持 支持
单引号字符串 不支持 不支持 支持
注释 不支持 支持 支持
尾逗号 不支持 不支持 支持
无引号键名 不支持 不支持 支持
十六进制数字 不支持 不支持 支持
NaN / Infinity 不支持 不支持 支持
扩展名 .json .jsonc .json5

选择原则很简单:跨进程传输一律标准 JSON,配置文件可以按编辑器支持程度选 JSONC 或 JSON5。 tsconfig.json、.vscode/settings.json 能写注释是因为编辑器按 JSONC 解析,把它们丢给严格的 JSON.parse 一样会报错。

JSON 能写注释吗?

不能。RFC 8259 的语法定义里没有注释产生式,任何 // 或 /* */ 都会导致解析失败。

想在配置里保留说明,有三种替代写法:

{
  "_comment": "以下超时单位是毫秒",
  "timeout": 3000,
  "retry": {
    "$doc": "重试次数,0 表示不重试",
    "count": 3
  }
}
  • 用 _comment 这类约定字段,读取方忽略即可;
  • 用 $ 前缀字段,减少与业务字段撞名的概率;
  • 干脆换 JSONC / YAML 存配置,只在运行时转成标准 JSON。

尾逗号为什么会报错?

因为语法里数组与对象都是「元素 + 分隔符」的结构,最后一个元素后面不允许再有逗号。

{
  "a": 1,
  "b": 2,
}

上面这段在 } 前多了一个逗号,错误位置通常报在逗号或右花括号上,具体取决于解析器的实现。 这也是从 JavaScript 迁移过来最容易踩的坑——JS 对象字面量允许尾逗号,JSON 不允许。

单引号可以吗?

不可以。字符串必须用双引号,'text' 属于语法错误。

如果数据是从 Python 的 repr()、Shell 脚本或手写配置里复制过来的,先用工具做一次转换, 不要指望解析器宽容处理。

键名可以不加引号吗?

不可以。对象的成员名必须是双引号包裹的字符串。

{ name: "json" }

上面的写法是 JavaScript 对象字面量,不是 JSON。加上引号才合法:

{ "name": "json" }

顶层可以是数组或字符串吗?

可以。RFC 8259 允许顶层是任意 JSON 值:对象、数组、字符串、数字、true、false、null 都合法。

但工程上建议顶层只用对象或数组,原因有两个:

  1. 兼容性:老版本解析器(RFC 4627 时代)要求顶层必须是对象或数组;
  2. 安全性:顶层是数组时历史上存在 JSON 劫持风险,现代浏览器已修复,但老系统的约定仍在。

所以 "hello" 是合法 JSON,["a", "b"] 也是,但接口返回 {"data": [...]} 更稳妥。

二、编码与字符串

文件开头的 BOM 会导致解析失败吗?

会。RFC 8259 明确要求 JSON 文本以 UTF-8 编码传输,且不得添加字节序标记(BOM)。

BOM 是文件最前面的三个字节 EF BB BF,肉眼完全不可见,但会出现在字符串的第一个位置, 导致解析器认为「第一个字符不是 {」而报错,而且报错行号往往指向第 1 行第 1 列,看起来莫名其妙。

Windows 记事本「另存为 UTF-8」默认就带 BOM。检查和清除:

# 检查:输出以 efbbbf 开头即为带 BOM
head -c 3 data.json | xxd

# 清除:用 tail 跳过前 3 字节
tail -c +4 data.json > clean.json

现代 JavaScript 的 JSON.parse 会容忍 BOM(V8 内部会跳过),但 Java、Go、Python 的严格实现不会, 这也是「同一文件在不同语言里结果不同」的常见原因。

中文需要转成 \uXXXX 吗?

不需要。RFC 8259 要求 JSON 文本是 UTF-8,中文可以直接出现,{"name": "张三"} 完全合法。

\uXXXX 只是等价的另一种写法,用于那些无法直接表示该字符的传输通道。判断标准:

  • HTTP 请求体、文件、数据库:直接写中文,省 3 字节/字;
  • HTTP 头、URL 参数、嵌入另一层字符串:用转义,避免编码问题。

唯一必须转义的是控制字符(U+0000 到 U+001F),例如换行必须写成 \n, 不能直接在字符串里放一个真实换行。

字符串里能直接放 emoji 吗?

可以,但要注意它占两个 UTF-16 码元。

emoji 和很多生僻字(如「𠮷」)超出了 BMP 范围,在 JavaScript 里 "😀".length === 2。 按字符数做校验时容易算错:

const text = "😀";
console.log(text.length);          // 2,UTF-16 码元数
console.log([...text].length);     // 1,码点数
console.log(Buffer.byteLength(text)); // 4,UTF-8 字节数

昵称长度限制如果按 .length 算,一个 emoji 就占了两个名额;后端按字节算又是 4 个。 对齐口径时要写清是码点、码元还是字节。

三、数字与特殊值

大整数 9007199254740993 为什么变成了 9007199254740992?

因为 JavaScript 的 JSON.parse 把数字转成了 IEEE 754 双精度浮点,安全整数上限是 2^53-1。

Number.MAX_SAFE_INTEGER === 9007199254740991  // true
9007199254740993                              // 实际得到 9007199254740992

只要数字超过 2^53-1,末位就可能被舍入。典型的受害者是雪花算法生成的 19 位用户 ID、 订单号、时间戳纳秒值。

三种修法,按推荐程度排序:

  1. 服务端把 ID 序列化成字符串:"id": "9007199254740993",从根上避免精度问题;
  2. 用无损解析库:JSON.parse 的替代实现可以保留大整数原文,例如 json-bigint;
  3. 只做展示时用正则提取:不解析整个文档,直接按文本匹配出目标字段。

注意 JSON.stringify 也有对称问题:超过安全整数范围的数字在序列化后可能已经不是原值了。

NaN 和 Infinity 能放进 JSON 吗?

不能。JSON 的数字产生式只允许十进制表示,NaN、Infinity、-Infinity 都不是合法 JSON 值。

JavaScript 的处理方式是「静默降级」:

JSON.stringify({ value: NaN });        // '{"value":null}'
JSON.stringify({ value: Infinity });   // '{"value":null}'

没有报错,NaN 直接变成 null。这就是「前端传了 NaN,后端收到 null」的经典现场。 需要传这类语义时,用字符串或约定的哨兵值显式表达:

{ "ratio": null, "ratioState": "not-a-number" }

数字能写成 1e3 或 0.5 吗?

能。JSON 数字支持小数与指数,1e3、1E3、0.5、-0.5 都合法。

但有几个限制:

  • 不能以 . 开头或结尾:.5 与 5. 都不合法,要写 0.5 和 5;
  • 不能有前导零:01 不合法;
  • 不能用十六进制:0x1F 不合法,那是 JSON5 的扩展;
  • -0 合法,但 JSON.stringify(-0) 输出 0,符号会丢。

四、对象语义

重复键会怎样?

行为由实现决定,RFC 8259 只说键名「应当」唯一,没有规定重复时的处理。

实践中绝大多数实现取最后一个值:

JSON.parse('{"a":1,"a":2}')  // { a: 2 }

但不能依赖它。Java 的某些库、Go 的 encoding/json、Python 的 json 在不同版本上行为有差异, 有的取第一个,有的报错,有的取最后一个。跨语言传输时把重复键当成错误处理最安全。

排查重复键不能靠肉眼——压缩过的 JSON 里同名键可能相隔几千字符。用脚本查:

# 提取同一层级出现多次的键,粗略检测
jq -r 'paths(scalars) | join(".")' data.json | sort | uniq -d

undefined 会变成什么?

分情况,而且是最容易出 bug 的地方:

JSON.stringify({ a: undefined });              // '{}'  键被整个丢掉
JSON.stringify({ a: undefined, b: 1 });        // '{"b":1}'
JSON.stringify([undefined]);                   // '[null]'  数组里变成 null
JSON.stringify({ a: function () {} });         // '{}'  函数同样被丢掉

同一个 undefined,在对象里是「字段消失」,在数组里是「变成 null」。 后端如果按「字段缺失」和「字段为 null」走不同分支,这里就会出现不一致。

需要显式表达「没有值」时,用 null 而不是 undefined。

{"__proto__": {...}} 解析后会发生什么?

JSON.parse 本身是安全的:__proto__ 会被当成普通自有属性,不会改原型。

危险的是解析之后的合并操作:

const patch = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign({}, patch);       // 某些实现会污染原型

正规做法是遍历自有属性并跳过 __proto__、constructor、prototype, 细节见 性能与安全常见问题。

常见问题

为什么同一个 JSON 文件在 Chrome 能解析、在 Java 里却报错?

通常是 BOM。现代 JavaScript 的 JSON.parse 会跳过开头的 BOM,而 Java、Go、Python 的严格实现不会,于是同一个文件在不同语言里得到不同结果。先用 head -c 3 data.json | xxd 确认是否以 EF BB BF 开头,再用 tail -c +4 清掉。

怎么把 JSON5 或 JSONC 的配置变成合法 JSON?

用工具做一次转换,产出标准 JSON 后再发给后端或写进接口请求体。tsconfig.json、.vscode/settings.json 能写注释,只是因为编辑器按 JSONC 解析,把它们丢给严格的 JSON.parse 一样会报错。

为什么不能依赖重复键的最后一次取值?

RFC 8259 只说键名「应当」唯一,没有规定重复时的处理,行为由实现决定。Java 的某些库、Go 的 encoding/json、Python 的 json 在不同版本上有的取第一个、有的报错、有的取最后一个,跨语言传输时把重复键当错误处理最安全。

五、速查

写法 合法 说明
{"a": 1,} 否 尾逗号
{'a': 1} 否 单引号
{a: 1} 否 键名缺引号
{"a": 1} // 注释 否 注释
"hello" 是 顶层字符串合法,但接口少用
{"a": 1e3} 是 支持指数
{"a": 01} 否 前导零
{"a": NaN} 否 非法字面量,stringify 会变 null
\uFEFF{"a":1} 否 BOM 不属于 JSON 文本
{"a":1,"a":2} 有争议 实现取最后一个,但不能依赖