合法的 JSON 为什么还会报错:逗号、数字与转义
合法的 JSON 为什么还会报错:逗号、数字与转义
JSON 的语法比看起来严格得多。解析器报的位置到底说明什么、长 ID 为什么会变值,以及格式化工具永远发现不了什么。
原创流程图
合法的 JSON 为什么还会报错:逗号、数字与转义
理解原理
检查后再进入下一步
检查边界
检查后再进入下一步
使用工具
检查后再进入下一步
JavaScript 里合法、JSON 里非法的写法有四类:最后一个元素后面的尾逗号、单引号字符串、不带引号的对象键,以及注释。NaN 和 Infinity 也不是合法数字,前导加号和前导零同样不允许。手写的 JSON 基本都栽在其中之一。如果这个配置文件是人写的而不是程序生成的,先检查每个数组和对象的最后一个元素——尾逗号是最常见的单一原因。
解析器报的是它放弃的偏移量,通常在真正的错误之后而不是错误本身上。字符串没闭合会报到文件末尾,因为解析器一直往后找收尾的引号。第 40 行的意外符号,往往意味着第 39 行少了一个逗号。先做格式化:结构缩进之后,肉眼找不配对的花括号,比数字符快得多。
JSON 没有规定数值精度,几乎所有解析器都把数字读成 IEEE-754 双精度。它只能精确表示到 2^53,约九千万亿。19 位的推特式 ID 或 Snowflake ID 超过了这个范围,会被静默四舍五入,末尾几位就变了。此时 JSON 依然合法,值却已经错了。任何装不进 53 位的标识都应该用字符串传输,用大整数存金额的最小单位也是同理。
规范没有规定同一个对象里出现两次相同键时该怎么办。多数解析器保留最后一个,有的保留第一个,也有少数直接报错。当请求体是靠字符串拼接或模板合并生成时,这一点尤其危险,因为重复在渲染结果里看不出来。键顺序也一样有歧义:对象按定义就是无序的,所以排序键适合用来比对两份数据,但绝不能用来承载含义。
字符串内部的反斜杠、双引号,以及 U+0020 以下的控制字符必须转义。\uXXXX 表示的是一个 UTF-16 码元,所以基本多文种平面之外的字符——emoji、生僻汉字——需要用两个转义组成的代理对。落单的代理项在技术上是畸形的,后续某个环节一定会变成替换字符。如果中文或日文变成了问号和方块,那么编码是在 JSON 参与之前就坏掉的,这份数据只是症状,不是原因。
格式化是给人读的,压缩是为了传输字节。一旦开启传输压缩,压缩带来的收益比看上去小得多,因为 gzip 和 brotli 处理重复空白很高效。应该在发送这一步压缩,源码仓库里保留格式化版本,绝不要把压缩后的内容当成给人编辑的文件存下来。
格式化只回答一个问题:这是不是格式良好的 JSON。它不知道必填字段有没有缺、某个字符串本该是数字、空数组表示「没有」还是「没加载」。这些是 schema 层面的问题。一份格式完美的数据照样可能被接口拒绝,所以格式化成功应该看成排查的开始,而不是结束。
常见问题
JSON 语法里根本没有这条规则。JavaScript 容忍它,JSON 不容忍,这也是手写文件解析失败最常见的原因。
它被解析成了双精度浮点数。超过 2^53 的整数无法精确表示,低位会被舍入。这种长度的标识应该用字符串传。
标准 JSON 不行。有些工具支持 JSON5 或 JSONC,但走接口的内容应该默认注释会导致解析错误。常见变通是加一个 "_comment" 键。
只有在没开传输压缩时才值得。gzip 已经消化掉了绝大部分美化排版的成本,通常可读性更划算。