技术笔记
JSON 是怎么被解析的:从一串字符到一棵值
格式化、压缩和树查看器看起来是三个按钮,底下其实是同一条流水线:先切词,再按文法收成值。这篇把这条路径拆开,并说明报错行号是从哪一层来的。
作者 Brook/更新于 2026-10-09/约 12 分钟
程序看到的不是树,是一串字符
把一段 JSON 粘进编辑器时,人眼会直接把它读成对象、数组和字段。解析器没有这个特权。它拿到的是一个 Unicode 码点序列,里面混着空格、换行、引号、冒号和反斜杠。在确认「这是一个对象」之前,它必须先决定每个字符属于哪一类记号。
所以格式化按钮并不是在「美化文本」。它先把文本丢掉结构,得到内存里的值,再按你选的缩进把值重新打印出来。压缩是同一条路的另一次打印:分隔符还在,空白被去掉。两边任何一个环节搞错,表现出来都是「点了按钮,结果不像预期」。
先切词,再谈结构
词法分析只回答一个问题:下一个记号是什么。它不关心这个记号在语法上合不合法。读到 " 就进入字符串状态,一直读到没有被转义的结束引号;读到 - 或数字就进入数字状态,直到字符不再可能属于数字;{ } [ ] : , 各自是一个单字符记号;true、false、null 必须整词匹配,多一个字母就是非法记号。
- 01输入字符序列含空白、换行和转义,还没有结构。
- 02词法记号流字符串、数字、字面量、分隔符各成一块。
- 03语法值树对象、数组和标量按文法嵌套起来。
- 04打印文本或视图缩进打印、压缩打印,或展开成树。
空白在这一层就被丢掉。JSON 允许空格、制表符、换行和回车出现在记号之间,但不允许出现在字符串外面的注释。所以 // 和 /* */ 不是「宽松 JSON」,而是词法错误。很多接口文档里的示例带注释,直接粘贴会在第一个斜杠处失败,失败点和注释位置是对齐的。
用栈把记号收成值
语法分析拿着记号流,按一条很短的文法往下走:一个值是对象、数组、字符串、数字、true、false 或 null。对象是若干「字符串键、冒号、值」,中间用逗号隔开;数组是若干值,同样用逗号隔开。实现上通常用一个栈记住「我现在正在填哪个容器,还缺键还是缺值」。
遇到 { 就压入一个新对象;遇到 } 就弹出,并检查它是不是正好停在一个完整成员之后。逗号后面必须再有成员,所以结尾多余的逗号会被拒绝。这和 JavaScript 对象字面量不同。浏览器里的 JSON.parse 故意比对象字面量更严,工具如果偷偷用 eval 或 Function 去跑,会把尾逗号、注释甚至代码也吃进去,那就不是在解析 JSON。
- 键必须是字符串。
{count: 1}里的 count 没有引号,词法阶段就会把它当成非法记号。 - 单个顶层值是合法的。
42和"ok"都能解析,格式化不会强行套一层对象。 - 逗号、冒号、括号必须成对且位置正确。缺一个就停在那个记号,而不是停在文件末尾。
数字、字符串和转义为什么特别容易错
字符串里的反斜杠只开启有限几种转义:引号、反斜杠、斜杠、退格、换页、换行、回车和制表符,以及 \u 后面恰好四位十六进制。\x61 和反斜杠加单引号是 JavaScript 的写法,不是 JSON。日志里常见的未转义换行,会让字符串状态一直延续到下一行,报错位置往往落在后面某个引号上,看起来像「后半段坏了」,其实开口在更早的地方。
数字不能有前导零,不能是 NaN 或 Infinity,小数点和指数的写法也有限制。更麻烦的是精度:JSON 不规定整数宽度,而 JavaScript 的数字是 IEEE 754 双精度,超过 2^53-1 的整数在解析时就会改值。雪花 ID、长订单号这一类字段,安全的做法是在解析前把它们当成字符串,或者用能保留大整数的解析器。格式化工具如果先走了普通 JSON.parse,再漂亮的缩进也救不回已经丢掉的低位。
三个工具停在同一条流水线的不同出口
格式化要的是重新打印后的文本,缩进只影响打印,不影响值。压缩同样打印,只是分隔符之间不放空白。树查看器停在值树上:它关心路径、类型和子节点,不关心原来有几个空格。三个工具可以共用一次解析,失败时也应该共用同一次错误。
- 01树查看按路径展开值展示对象和数组的层级,点开嵌在字符串里的 JSON 时,是对那个字符串再走一遍同一条流水线。
- 02格式化按缩进重新打印2 空格、4 空格或制表符只改变空白,键的顺序和数字的文本形式取决于实现有没有保留原始写法。
- 03压缩去掉记号之间的空白字符串内部的空格保留。压缩不是加密,也不是缩短键名。
- 04解析词法 + 语法失败时给出记号位置。行号和列号是从字符偏移换算出来的,不是猜测。
嵌在字符串里的 JSON 是另一层。日志字段、消息队列的 payload,经常是「字符串,其内容恰好又是 JSON」。外层解析成功之后,那个字段的类型仍是字符串。树查看器如果要往下钻,必须对这个字符串再跑一次解析,并且把新的路径接在原路径后面。外层失败和内层失败要分开报,否则用户会以为整份文档坏了。
实现时值得守住的边界
解析器应该拒绝自己不认识的语法,而不是尽量猜。猜对了一次尾逗号,下次就会把真正的残缺文档修成另一个意思。错误信息要带位置:至少是从 1 计的行和列,最好还能在附近截一小段原文。没有位置的「解析失败」几乎没法修。
打印和解析要分开测试。解析测试看非法输入是否被拒绝、合法输入的值是否正确;打印测试看缩进、键序和 Unicode 转义是否稳定。混在一起时,很容易用「看起来差不多的字符串」掩盖值已经被改掉的问题。本站的 JSON 工具走的就是这条边界:先在浏览器里解析,失败就定位到行,成功再按你选的形式打印或展开。