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

结论:排查 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 ) ] "]"
要点只有六个:
- 对象用花括号,成员之间用逗号,最后一个成员后面不允许逗号;
- 数组用方括号,元素之间用逗号,同样不允许尾逗号;
- 数字不允许前导
+、不允许前导零(01非法)、不允许只有小数点(1.非法)、不允许NaN与Infinity; - 字符串必须用双引号,单引号非法;
- 布尔与 null 是小写字面量,
True、NULL都非法; - 只允许空白字符
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 节说了三件事:
- 用于交换的 JSON 文本必须用 UTF-8 编码;
- 实现不得在文本开头添加 BOM(
EF BB BF); - 解析器可以忽略 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 里非法,所以「配置文件能跑、接口传过去就报错」几乎总是方言问题。需要互转时先把注释、单引号和尾逗号清掉,再交给标准解析器。