技术笔记

JSON 是怎么被解析的:从一串字符到一棵值

格式化、压缩和树查看器看起来是三个按钮,底下其实是同一条流水线:先切词,再按文法收成值。这篇把这条路径拆开,并说明报错行号是从哪一层来的。

作者 Brook/更新于 2026-10-09/约 12 分钟

程序看到的不是树,是一串字符

把一段 JSON 粘进编辑器时,人眼会直接把它读成对象、数组和字段。解析器没有这个特权。它拿到的是一个 Unicode 码点序列,里面混着空格、换行、引号、冒号和反斜杠。在确认「这是一个对象」之前,它必须先决定每个字符属于哪一类记号。

所以格式化按钮并不是在「美化文本」。它先把文本丢掉结构,得到内存里的值,再按你选的缩进把值重新打印出来。压缩是同一条路的另一次打印:分隔符还在,空白被去掉。两边任何一个环节搞错,表现出来都是「点了按钮,结果不像预期」。

先切词,再谈结构

词法分析只回答一个问题:下一个记号是什么。它不关心这个记号在语法上合不合法。读到 " 就进入字符串状态,一直读到没有被转义的结束引号;读到 - 或数字就进入数字状态,直到字符不再可能属于数字;{ } [ ] : , 各自是一个单字符记号;true、false、null 必须整词匹配,多一个字母就是非法记号。

  1. 01输入字符序列含空白、换行和转义,还没有结构。
  2. 02词法记号流字符串、数字、字面量、分隔符各成一块。
  3. 03语法值树对象、数组和标量按文法嵌套起来。
  4. 04打印文本或视图缩进打印、压缩打印,或展开成树。
图 1 JSON 文本进入格式化或树查看之前,要先经过这四步。格式化和压缩只是最后一步的两种打印。

空白在这一层就被丢掉。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,再漂亮的缩进也救不回已经丢掉的低位。

三个工具停在同一条流水线的不同出口

格式化要的是重新打印后的文本,缩进只影响打印,不影响值。压缩同样打印,只是分隔符之间不放空白。树查看器停在值树上:它关心路径、类型和子节点,不关心原来有几个空格。三个工具可以共用一次解析,失败时也应该共用同一次错误。

  1. 01树查看按路径展开值展示对象和数组的层级,点开嵌在字符串里的 JSON 时,是对那个字符串再走一遍同一条流水线。
  2. 02格式化按缩进重新打印2 空格、4 空格或制表符只改变空白,键的顺序和数字的文本形式取决于实现有没有保留原始写法。
  3. 03压缩去掉记号之间的空白字符串内部的空格保留。压缩不是加密,也不是缩短键名。
  4. 04解析词法 + 语法失败时给出记号位置。行号和列号是从字符偏移换算出来的,不是猜测。
图 2 一次解析可以喂给三个出口。报错应该发生在词法或语法层,而不是等到打印时才含糊地说失败。

嵌在字符串里的 JSON 是另一层。日志字段、消息队列的 payload,经常是「字符串,其内容恰好又是 JSON」。外层解析成功之后,那个字段的类型仍是字符串。树查看器如果要往下钻,必须对这个字符串再跑一次解析,并且把新的路径接在原路径后面。外层失败和内层失败要分开报,否则用户会以为整份文档坏了。

实现时值得守住的边界

解析器应该拒绝自己不认识的语法,而不是尽量猜。猜对了一次尾逗号,下次就会把真正的残缺文档修成另一个意思。错误信息要带位置:至少是从 1 计的行和列,最好还能在附近截一小段原文。没有位置的「解析失败」几乎没法修。

打印和解析要分开测试。解析测试看非法输入是否被拒绝、合法输入的值是否正确;打印测试看缩进、键序和 Unicode 转义是否稳定。混在一起时,很容易用「看起来差不多的字符串」掩盖值已经被改掉的问题。本站的 JSON 工具走的就是这条边界:先在浏览器里解析,失败就定位到行,成功再按你选的形式打印或展开。

文中对应的工具

继续读