JSON RFC 8259:绊倒生产环境解析器的细微之处
JSON 比你以为的更小
RFC 8259 里的 JSON 文法很小:对象、数组、字符串、数字,以及三个字面值 true、false、null。这份简单正是它无处不在的原因——也是两个惊喜会让人措手不及的原因。第一,任何值都能当整份文档:42、"hello"、true 各自都是完整、合法的 JSON。较旧的 RFC 4627 要求顶层必须是对象或数组,所以有些旧工具仍会拒绝裸值。
第二,规范刻意把好几种行为留作未指定。它精确定义了语法,却对重复键、数字精度或排序着墨甚少。生产环境的解析器以不同方式填补这些空缺,而互通性 bug 正是住在那里。
重复键:未定义行为
RFC 8259 说对象名称应该(SHOULD)唯一,而非必须(MUST)。当键重复时,结果未指定,所以解析器各异:多数取最后一个值、有些保留第一个、少数抛错、极罕见的会两个都留。依规范而言,没有一个是错的。
这是一个真实的安全隐患。如果一个服务读第一个 role 来验证 {"role":"user","role":"admin"},而另一个服务依最后一个行事,攻击者就能让权限溜过验证。在任何结果攸关的地方都把重复键当成错误,永远别倚赖哪个值“胜出”。
数字:没有整数,只有麻烦
JSON 只有单一数字类型。RFC 并未定义精度或范围——它只指出,为了互通,实现被预期能处理符合 IEEE 754 双精度的值。实务上这意味着任何超过 2^53(即 9007199254740992)的值都可能失去精度。
经典 bug:像 9007199254740993 这样的 64 位 ID,在 JavaScript 里经 JSON.parse 后会变成 9007199254740992,因为数字是双精度浮点。解法是把大整数以字符串携带。还有几条人们会忘的规则:不能有前导零(007 非法)、不能有前导 +、不能有结尾小数点(1. 非法),而 NaN 与 Infinity 不是合法 JSON——把它们序列化成 null 或字符串。
字符串、Unicode 与编码
字符串必须用双引号,而 U+0000 到 U+001F 的控制字符必须被转义。正斜杠可选择性地转义成 \/,这就是为何有些编码器会输出 <\/script>——一种避免弄破 HTML script 标签的合法做法。RFC 8259 规定系统间交换的 JSON 必须用 UTF-8。
基本多文种平面(BMP)以外的字符,例如大多数 emoji,要写成一对代理(surrogate)\u 转义,而非单一个。孤立、未配对的代理在技术上非法,但有些解析器会接受——一个只有当你的数据跨进更严格的实现时才会浮现的可移植性陷阱。
看起来像 JSON 但不是的东西
最常见的错误来自把 JSON 当成 JavaScript。结尾逗号非法。JSON 里不存在注释——JSON5 和 JSONC 加了它们,但严格的 RFC 8259 解析器会拒绝。单引号字符串和未加引号的对象键同样非法,无论看起来多眼熟。
更微妙的是字节顺序标记(BOM)。RFC 8259 说生产者绝不可(MUST NOT)加 BOM、消费者可以(MAY)忽略一个,但 BOM 不是 JSON 的一部分,所以文件开头的 UTF-8 BOM 可能让严格解析器在第一个字节就失败。不具意义的空白仅限空格、tab、换行(LF)与回车(CR)——别无其他。
要点
在任何安全或正确性决策取决于值的地方拒绝重复键。
把 64 位整数以字符串携带,使它们能在像 JavaScript 那样的双精度解析器中存活。
别倚赖对象键的顺序——规范把对象定义为无序,即使多数解析器会保留插入顺序。
记住在 RFC 8259 下任何值都是合法的顶层 JSON,不只是对象和数组。
如果你想要注释或结尾逗号,你要的是 JSON5/JSONC——在严格解析器看到数据之前先转换。本站的 JSON 格式化工具会对照严格的 RFC 8259 文法验证,所以这些问题会立即浮现。