JSON 工具箱

从 RFC 8259 读懂 JSON 语法边界

结论:把 RFC 8259 的语法产生式翻译成人话:哪些字符能直接出现、哪些必须转义、规范在哪些地方刻意留白。

入门约 11 分钟发布于 2026-10-30更新于 2026-10-30
从 RFC 8259 读懂 JSON 语法边界封面

结论:排查 JSON 语法问题时先定位第一个非法字符,再判断它属于单引号、尾逗号、注释还是控制字符,不要从头重读整段;编码层面则先确认 UTF-8 且无 BOM,超过 2^53 - 1 的整数改用字符串承载。

规范的正文只有十几页,但每一句「MUST」「SHOULD」「MAY」背后都藏着一个曾经让团队通宵的线上问题。

一、三份文本的关系

学 JSON 常被三个编号绕晕:RFC 4627、RFC 7159、RFC 8259,再加上 ECMA-404。关系其实很清楚:

文本 时间 地位 关键差异
RFC 4627 2006 已被取代 顶层必须是对象或数组
RFC 7159 2014 已被取代 顶层放宽为任意 JSON 值
RFC 8259 2017.12 现行标准 明确 UTF-8、补充互操作性建议
ECMA-404 2013,第二版 2017.12 语法规范 只定义语法,不涉及编码与传输

读哪一份? 只看 RFC 8259 就够了,ECMA-404 与它的语法部分等价,区别在于 RFC 8259 还规定了「用 UTF-8 传输」和「实现者应当注意的互操作问题」。

二、六种值类型

规范的语法产生式可以浓缩成一张图:

JSON-text  = ws value ws
value      = object / array / number / string / "true" / "false" / "null"
object     = "{" [ member *( "," member ) ] "}"
member     = string ws ":" ws value
array      = "[" [ value *( "," value ) ] "]"

要点只有六个:

  1. 对象用花括号,成员之间用逗号,最后一个成员后面不允许逗号;
  2. 数组用方括号,元素之间用逗号,同样不允许尾逗号;
  3. 数字不允许前导 +、不允许前导零(01 非法)、不允许只有小数点(1. 非法)、不允许 NaN 与 Infinity;
  4. 字符串必须用双引号,单引号非法;
  5. 布尔与 null 是小写字面量,True、NULL 都非法;
  6. 只允许空白字符 0x20(空格)、0x09(制表)、0x0A(换行)、0x0D(回车)出现在结构之间。

下面这段是合法 JSON,很多人第一次看到会以为错了——因为它是顶层的裸字符串:

"just a string"

RFC 7159 之后顶层确实可以是任意值。但接口里仍建议只用对象或数组,原因在第六节。

三、字符串里到底能直接放什么

这是最容易被「我本地能跑」骗过去的部分。规范对字符串的规定是:

  • 允许直接出现的字符:%x20-21、%x23-5B、%x5D-10FFFF;
  • 也就是说,双引号 "(0x22)与反斜杠 \(0x5C)必须转义;
  • 所有小于 0x20 的控制字符必须转义,包括换行、制表、回车;
  • 可用的转义序列只有 \"、\\、\/、\b、\f、\n、\r、\t、\uXXXX。
{
  "ok": "可以直接写中文、emoji 🎉 和全角符号",
  "escaped": "换行写在这里\n制表\t完成",
  "control": "\u0007 是响铃,必须转义",
  "quote": "他说:\"别用单引号\""
}

\/ 是历史遗留的转义(防止 </script> 提前闭合),现代场景基本不需要,但解析器必须支持。

四、编码:UTF-8、无 BOM

RFC 8259 第 8.1 节说了三件事:

  1. 用于交换的 JSON 文本必须用 UTF-8 编码;
  2. 实现不得在文本开头添加 BOM(EF BB BF);
  3. 解析器可以忽略 BOM 以提高健壮性。

现实中的两种典型故障:

  • Windows 记事本另存为「UTF-8 带 BOM」,文件被 JSON.parse 拒绝,报 Unexpected token;
  • 用 GBK 保存的中文 JSON,在只按 UTF-8 解码的链路上变成乱码,但语法居然还能过,于是错误延后到业务层。

处理方式很朴素:读文件时先剥掉 BOM,再显式按 UTF-8 解码;写入时确保不带 BOM。

/**
 * 去掉字符串开头的 UTF-8 BOM。
 * @param text 原始文本
 * @returns 不含 BOM 的文本
 */
function stripBom(text: string): string {
  return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
}

顺带提一句媒体类型:application/json 不再接受 charset 参数,写了虽然不报错,但属于过时写法。

五、规范刻意留白的三处

规范没有把话说死的地方,正是实现差异的来源。

1. 数字的范围与精度

RFC 8259 只说「本规范不限制数字的范围与精度」,同时指出大多数实现用 IEEE 754 双精度。 这意味着同一个 9223372036854775807,在 JavaScript 里会变成 9223372036854776000,在 Java 的 long 里却精确。

结论:超过 2^53 - 1 的整数、需要精确到小数后多位的金额,一律用字符串承载,并在 schema 里写清 pattern。

2. 重复键

规范原文是「对象中的名称应当(SHOULD)唯一」,没有规定重复时怎么办。

  • JSON.parse 取最后一个;
  • 部分解析器取第一个;
  • 有的库直接报错。

结论:不要写出重复键,也不要在校验器里依赖某一种行为。构建期加一条「重复键检测」比事后排查便宜得多。

3. 深度与长度上限

规范没有规定嵌套深度上限,但所有实现都有。V8 在几万层嵌套时会抛 RangeError: Maximum call stack size exceeded。 服务端必须自己设限,这是安全要求而不是规范要求。

六、用反例检验理解

下面每一行都是非法 JSON,可以拿去喂给解析器,观察错误信息是否友好:

{ 'name': 'json' }        // 单引号
{ name: "json" }          // 键名未加引号
{"a": 1,}                 // 尾逗号
[1, 2, 3,]                // 数组尾逗号
{"a": 01}                 // 前导零
{"a": +1}                 // 前导加号
{"a": .5}                 // 缺少整数部分
{"a": NaN}                // 非标准字面量
{"a": undefined}          // 非 JSON 值
{"a": 1} // 注释          // 注释

一个有意思的边界:{"a": 1} 后面跟注释在 JSON5 里合法,在 JSONC 里合法,在标准 JSON 里非法。 所以「配置文件能跑、接口传过去就报错」几乎总是方言问题。

七、落地检查清单

  • [ ] 所有对外传输的 JSON 使用 application/json,不带 charset;
  • [ ] 文件读写链路剥 BOM、显式 UTF-8;
  • [ ] 超过 2^53 的整数用字符串,schema 里标注;
  • [ ] 构建期检测重复键;
  • [ ] 服务端限制请求体大小与嵌套深度;
  • [ ] 对外接口的顶层值使用对象或数组,不用裸标量。

常见问题

如何快速判断一段文本符不符合 JSON 语法?

把文本交给解析器或在线解析工具,看它报出的第一个非法字符位置,再对照 RFC 8259 的六种值类型与空白字符规则逐一确认。顶层允许任意 JSON 值,但结构之间只允许空格、制表、换行与回车这四种空白。只要出现单引号、未加引号的键、尾逗号或注释,就说明它不遵守标准 JSON。

为什么 JSON 必须用 UTF-8 且不能带 BOM?

RFC 8259 第 8.1 节规定,用于交换的 JSON 文本必须用 UTF-8 编码,实现不得在开头添加 BOM,解析器可以忽略 BOM 以提高健壮性。带 BOM 的文件被 JSON.parse 拒绝时会报 Unexpected token,肉眼却看不出异常。因此读文件时先剥掉 BOM,再显式按 UTF-8 解码。

是否可以用 JSON5 或 JSONC 绕过这些语法限制?

可以,但只限配置文件这类本地读取的场景,对外交换仍应使用标准 JSON。原文的反例表明,{"a": 1} 后面跟注释在 JSON5 和 JSONC 里合法、在标准 JSON 里非法,所以「配置文件能跑、接口传过去就报错」几乎总是方言问题。需要互转时先把注释、单引号和尾逗号清掉,再交给标准解析器。