JSON
JavaScript Object Notation 的缩写,一种轻量级数据交换格式,具有人类和机器都易于读写的结构。
JSON (JavaScript Object Notation) 是一种以键值对形式表示数据的轻量级文本数据交换格式。该书写格式由 Douglas Crockford 于 2001 年整理成形,而作为规范被正式整理则在此之后。2006 年的 RFC 4627 是最初的标准化,2013 年出了 ECMA-404 第 1 版,2017 年出了 RFC 8259 与 ECMA-404 第 2 版。截至 2026 年应当参照的是 RFC 8259 和 ECMA-404 第 2 版这两份文件,二者写法不同,但定义的格式是同一个。虽然语法源自 JavaScript,但它是不依赖语言的通用格式,被 Python、Java、Go、Ruby 等事实上所有编程语言所支持。
JSON 能处理的数据类型有 6 种:字符串 (用双引号包围)、数字 (整数与浮点数)、布尔值 (true/false)、null、数组 (方括号)、对象 (花括号)。这种单纯性是 JSON 最大的优势,也是它成为 REST API 响应格式事实标准的原因。
与 XML 相比,JSON 不需要标签的开始与结束,因此描述量更少,解析也更快。同样的数据,比起要把键名在开始标签和结束标签中写两遍的 XML 会更短。不过削减率会随键名长度、嵌套深度以及是否使用属性而大幅变化,因此并不是一个可以当作固定比率来记住的数字。如果要把大小作为判断依据,请把实际数据分别以两种格式输出,用施加 gzip 之后的字节数来比较。对于相同结构反复出现的数据,gzip 会吸收标签名的重复,所以压缩后的差距不会像压缩前那么大。另一方面,XML 在模式定义 (XSD) 和命名空间支持上更为完善,在要求严格数据验证的场景中依然会被选用。YAML 在 1.2 版本中采取了把 JSON 作为严格超集纳入的方针,JSON 文本可以原样作为 YAML 读取。由于具备注释和锚点功能,它作为配置文件更受欢迎,但依赖缩进的语法有着容易在复制粘贴时引发错误的缺点。另外,这种包含关系并非完全等价,例如同一个键重复出现的 JSON 在 YAML 的规范上属于不合法 (有的实现会当作错误,也有的实现会默默只取其中一个值)。
JSON 有若干制约。它不支持注释,因此作为配置文件时有时会使用 JSON5 或 JSONC (JSON with Comments)。由于不存在日期类型,日期时间数据按惯例表示为 ISO 8601 格式的字符串 (例如 "2025-01-15T09:30:00Z")。日期时间字符串若省略末尾的 Z 或偏移量,接收方是解释为当地时间还是解释为 UTC 会导致结果偏差,所以要连时差一起写上。此外,由于不允许末尾的逗号 (trailing comma),在数组或对象最后一个元素之后加逗号会造成语法错误。
即使格式本身单纯,由于规范把细节交给了实现,实务中绊倒人的地方大多是以下 2 点。第一是数字的精度,RFC 8259 没有规定可处理的位数和范围,多数实现以 IEEE 754 的双精度 (binary64) 为前提。同一份 RFC 举出的可互操作整数上限是 2 的 53 次方减 1,即 9,007,199,254,740,991 (负的一侧也是同样的绝对值),把超过这个范围的 ID 当作数字传递,低位数字会被舍入,从而指向另一条记录。19 位的 ID 或账号号码,从一开始就作为字符串来传递才安全。第二是键的重复,RFC 8259 只写到对象内的名称应当唯一,并未禁止重复本身。重复时的行为交由实现决定,只取最后出现的值的、报错的、把全部都返回的实现混杂存在。由于这也被用作绕过验证的手法,因此既需要在生成侧不制造重复,也需要在接收侧检测出重复并加以拒绝。
实务中,JSON 的验证与模式定义广泛使用 JSON Schema。把 API 的请求与响应结构用 JSON Schema 定义下来,客户端与服务器之间的数据契约就会明确,可以防止不正确的数据混入。
在安全方面,绝对应当避免用 eval() 解析来自不可信来源的 JSON。务必使用 JSON.parse(),以防止不正当代码的执行。另外,如果用字符串拼接来组装 JSON,输入中包含的双引号或花括号就可能改写数据的结构本身。嵌入值时不要自行做转义,而是交给各语言的序列化器 (JavaScript 则是 JSON.stringify()) 才可靠。
从字符计数的角度来看,JSON 的键名、花括号、方括号、双引号、冒号、逗号等语法元素都会影响数据大小。关于字符编码,RFC 8259 规定在封闭环境之外交换的 JSON 必须以 UTF-8 编码,并且禁止在开头加上 BOM (接收方可以忽略 BOM,是这样的处理)。字节数的估算以 UTF-8 为前提计算就足够了。通过压缩 (minify) 去除不必要的空白和缩进可以削减大小,再组合 gzip 压缩还能大幅缩减传输大小。在包含日语的响应中,是把文字原样写出,还是转义为反斜杠后接 u 与 4 位十六进制数的形式 (如 \u3042),字节数也会不同。UTF-8 下日语 1 个字符是 3 字节,转义后变成 6 字节,因此如果库的设置在机械地转义非 ASCII 字符,以日语为主的响应会膨胀到大约 2 倍。在优化 API 响应大小时,排除不必要的字段和缩短键名也是有效的手法。