过去12个月,GitHub上关于"JSON parse error"的issue超过180万个。平均每30秒就有一个开发者因为少删了一个逗号,在凌晨两点对着屏幕骂街。

JSON的语法规则只有一页A4纸,但它的报错信息却像迷宫——指向的行号往往是错的,真正的问题藏在三行之前。这篇文章把90%的解析失败浓缩成一张图,看完你就知道该盯哪里。

打开网易新闻 查看精彩图片

一图读懂:JSON的"四宗罪"

想象一张简单的流程图。中心是"JSON解析失败",四个分支向外延伸:尾随逗号、单引号、注释、特殊值。每个分支旁边标注着出现频率和典型场景。这就是我们要拆解的核心——四个错误类型覆盖了绝大多数崩溃现场。

我们从最泛滥的那个开始。

分支一:那个该死的逗号

JavaScript开发者最容易栽这里。对象最后一个属性后面加逗号,在JS里完全合法,甚至ES2017之后还成了推荐写法。但JSON诞生于2001年,它的语法规范冻结在RFC 8259,不认这个"现代化"习惯。

错误示例:

{ "name": "Alice", "role": "admin", "active": true, }

注意true后面那个逗号。肉眼扫过去很容易忽略,因为前面三行看起来完美无缺。更坑的是解析器的报错——它通常指向右花括号},告诉你"这里有问题"。你盯着第4行看了五分钟,最后发现凶手在第3行末尾。

修正版本:

{ "name": "Alice", "role": "admin", "active": true }

手工写配置的时候,养成一个肌肉记忆:每加一个字段,先写逗号再写键名。如果是最后一个,删掉逗号。反过来操作——先写内容再补标点——失败率会翻倍。

复制粘贴是另一个高危场景。你从同事的代码里抄了一段,对方用的是JavaScript对象字面量,你直接丢进JSON文件。编辑器的高亮可能都不会变色,因为两者实在太像了。

分支二:单引号的惯性陷阱

Python和JavaScript程序员,这是写给你们的。

错误示例

{ 'username': 'bob', 'permissions': ['read', 'write'] }

看起来人畜无害,甚至配色方案都认它是字符串。但RFC 8259第7节写得清清楚楚:字符串必须用双引号界定。单引号在JSON里没有特殊地位,只是一个普通字符——如果它出现在值的位置,直接触发解析失败。

修正版本:

{ "username": "bob", "permissions": ["read", "write"] }

这个错误的狡猾之处在于"视觉欺骗"。很多编辑器的JSON模式并不会对单引号报错,因为它们默认你在写JavaScript。直到运行时崩溃,你才发现文件扩展名是.json而不是.js。

有个快速自检法:全选你的JSON,搜索单引号字符'。如果结果不为零,你已经找到了至少一个潜在故障点。

分支三:注释的幽灵

这是最冤的一种失败。你从官方文档复制了一段配置,文档作者好心加了注释说明每个字段的用途。你满怀感激地粘贴,然后解析器炸了。

错误示例:

{ // database settings "host": "localhost", "port": 5432, /* default postgres port */ "database": "myapp" }

JSON的设计者Douglas Crockford在2009年明确拒绝加入注释支持。他的理由很技术官僚:注释可能被用来传递指令,破坏数据纯粹性。结果是无数开发者被迫在"人类可读"和"机器可读"之间二选一。

修正版本:

{ "host": "localhost", "port": 5432, "database": "myapp" }

如果你确实需要保留说明,有几个替代方案。YAML原生支持注释,TOML也支持。或者采用一个约定俗成的做法:加一个以_开头的假字段,比如"_comment": "这是数据库配置"。解析器会把它当普通数据处理,人类读者能看到注释——虽然有点丑,但能用。

有些团队会用预处理脚本,在提交前自动剥离注释。这相当于给JSON开了个后门,但增加了构建复杂度。要不要用,取决于你的团队更讨厌配置噪音,还是更讨厌构建步骤。

分支四:JavaScript的"特产"值

undefined、NaN、Infinity——这三个是JavaScript的"土特产",JSON护照上没它们的签证。

错误示例:

{ "count": NaN, "result": undefined, "ratio": Infinity }

这些值在JS运行时常出现。除以零得到Infinity,变量未赋值是undefined,非法数学运算返回NaN。当你用JSON.stringify()序列化一个对象时,如果里面混进了这些家伙,结果会出乎意料。

实际上JSON.stringify()对它们有默认处理:undefined和函数会被直接忽略,NaN和Infinity变成null。但很多人不知道这个行为,或者手动拼接字符串时直接写进了字面量。

修正版本:

{ "count": 0, "result": null, "ratio": null }

关键决策点是语义选择。NaN表示"不是数字",转成0会丢失信息,转成null会丢失类型。如果你的下游系统需要区分"零值"和"无效值",得在协议层额外约定,不能指望JSON本身。

一个防御性编程技巧:给JSON.stringify()传第二个参数,一个replacer函数。遇到特殊值时抛出错误或者转成特定标记,而不是默默变成null。

从图到工具:怎么少加班

知道坑在哪只是第一步。真正省时间的是让工具帮你盯梢。

编辑器层面,VS Code装JSON Language Features扩展,开启"Trailing Commas"警告。Vim用户可以用ALE或者Coc,配置jsonlint作为linter。这些工具在你保存文件时就标红,比运行时崩溃便宜得多。

CI/CD层面,把jsonlint或者jq --exit-status写进预提交钩子。前者是纯语法检查,后者还能验证结构。一个典型的husky配置:

{ "hooks": { "pre-commit": "find . -name '*.json' -exec jsonlint -q {} +" } }

运行时层面,如果必须解析不可信来源的JSON,用try-catch包裹,并在catch块里记录原始字符串。生产环境的日志经常把错误消息截断,保留原始输入能帮你事后复盘。

更激进的方案是放弃JSON。配置文件用YAML或TOML,它们支持注释、尾随逗号可选、对人类更友好。内部服务间通信如果控制得了两端,Protocol Buffers或者MessagePack能省带宽和解析开销。JSON的优势是"人人会读",但当这个优势变成"人人会写错",就得重新算账。

为什么这件事值得较真

单个JSON错误可能只浪费你五分钟。但乘以团队规模、乘以配置变更频率、乘以"在我机器上没问题"的调试会话,这个数字会变得很难看。

更隐蔽的成本是认知负荷。当你养成"JSON可能有问题"的怀疑习惯,每次粘贴配置都要人工扫描一遍——这种低效的警觉消耗注意力,让你没精力处理真正复杂的bug。

好的工程实践是把高频低智错误交给自动化,把大脑内存留给不可预测的问题。JSON的四个语法坑,属于那种"一旦知道就永远知道"的知识,值得一次性投资建立防线。

你现在用的编辑器,对JSON错误的提示够及时吗?