← 返回博客
语言: English 中文
Format 2026-06-01 10 分钟

JSON 只有六种类型:简单语法如何制造生产陷阱

Douglas Crockford 在 2001 年发布了 json.org,规范一页就讲完。他坚持自己**没有发明** JSON——他只是从 JavaScript 对象字面量语法里抠出一个子集、给它起了个名字、然后**拒绝**给它再加任何东西。二十五年后,正是这种“拒绝”让 JSON 无处不在,而 XML 进了养老院。“我们不加新特性”这个代价,每天都被那些想在配置文件里写注释的开发者付着。

JSONRFC 8259ECMA-404JSON5NDJSONJSON Pointer

大多数开发者第一次接触 JSON 时是把它当作"API 返回的格式"。这个框架是个陷阱,因为它暗示 JSON 是个有语义的协议。它不是。JSON 是语法——六条规则,把六种值排成一棵文本树。其他东西全都是读它的人加在上面的约定。

理解这条区分,几乎能解释你在生产环境里见到的每一个 JSON bug。

文法写在餐巾纸上就够

JSON 有六种值类型:对象、数组、字符串、数字、布尔、null。对象是用 {} 围起来的无序键值对集合。数组是用 [] 围起来的有序列表。字符串是双引号包裹、有少量转义序列。数字是十进制字面量,可选小数与指数部分。布尔是 truefalsenull 表示"没有"。

这就是整个语言。没有语句、没有表达式、没有函数、没有引用、没有日期、没有整型 vs 浮点、没有二进制、没有注释。ECMA-404 是规范文本,全文 10 页,其中大部分还是铁路图。

实际存在的两份规范是 RFC 8259(IETF 版本,2017 年至今为最新版,取代了 RFC 7159 与 RFC 4627)和 ECMA-404(标准化机构版本,更薄)。两者文法一致;对"什么算合法的顶层值"有微小分歧——除非你在写解析器,否则无关紧要。

JSON 故意不要的那些东西

Crockford 在拒绝扩展这件事上立场极其稳定。理解每个"不要"背后的论证很有价值,因为同一生态位里几乎所有其他格式都做了相反的选择,并且都付出了代价。

  • 不要注释。 Crockford 的论证:一旦允许注释,人就会把解析器指令塞进去,注释就变成了协议的一部分。他曾说删掉注释是"我唯一有点后悔的事情"。大多数开发者觉得这是个错误——但考虑到配置文件生态后来的混乱(下文会讲),他们大概率错了。
  • 不要尾随逗号。 允许尾随逗号能让 diff 更干净——对象 / 数组的每一行都变成自包含编辑,而不是末行特殊。JSON5 加了。标准 JSON 解析器一律拒绝。
  • 不要日期。 JSON 里的日期是字符串。约定上用 RFC 3339 / ISO 8601(2026-06-01T12:00:00Z),但规范不强制,你会遇到返回 Unix 毫秒、Unix 秒、/Date(123456)/、本地化打印日期的 API。没有带外信息时根本无法区分。
  • 不要整型。 JSON 只有一种数字类型。1 是整型还是 1.0 是浮点,由解析器决定。JavaScript 的 JSON.parse 都返回 number,也就是 IEEE 754 双精度浮点,没法精确表示大于 2^53 - 1 的整数。这是最常见的"安静 JSON bug"的根源——下面会讲。
  • 不要 NaN / Infinity 它们不是合法 JSON。JavaScript 的 JSON.stringify({x: NaN}) 会序列化成 null——这是一个完全不同的值,下游期望是数字的解析器会在沉默中翻车。
  • 不要二进制。 想嵌字节?Base64 一下塞进字符串。付 33% 的体积税。或者换格式。
  • 不要引用 / 循环。 给一个有循环的对象图喂 JSON.stringify,解析器抛错。没有像 YAML 锚点(&id / *id)那样的语法。

每一项"不要"都有真实代价。好处是 JSON 解析器小、快、可预测,每种语言都有支持,没有任何一处需要协商的歧义。Crockford 当年的赌注是"格式层简单 > 特性多"。他赢了。

数字陷阱

这是几乎所有人在生产环境都会被咬一次的问题。

JSON.parse('{"id": 12345678901234567890}').id
// => 12345678901234570000

JavaScript 的 JSON.parse 把大整数四舍五入到最近可表示的双精度浮点。Twitter 在 2010 年前后吃过这个亏:tweet ID 跨过 2^53 之后,客户端在没有任何报错的情况下损坏了 ID,对话回复线就此断掉。他们后来加了一个字符串字段 id_str,并要求所有客户端用这个字段。

JavaScript 之外的语言大多处理得更好。Python 的 json.loads 返回任意精度的 int。Go 的 encoding/json 在你用 json.Number 时也行。Java 的 Jackson 可以配置成返回 BigInteger。但格式本身不告诉解析器哪个才对,所以生产者和消费者必须在带外达成一致。

务实规则:整数只要可能超过 2^53 - 1(= 9,007,199,254,740,991),就用字符串序列化。 数据库 ID、交易 ID、所有 snowflake 形态的东西。每条 ID 多花 6 字节,换一个不会在 JavaScript 端悄悄损坏数据的系统。

对象键的唯一性

RFC 8259 说:"对象内键名 SHOULD 唯一。"注意是 SHOULD,不是 MUST。

{"foo": 1, "foo": 2}

技术上是合法 JSON。行为是"实现自定义"。JavaScript 的 JSON.parse 取最后一个值({foo: 2})。Python 的 json.loads 同样。Java 的 Jackson 默认取最后。Go 的 encoding/json 解码到 struct 取最后,到 map 也取最后。

几乎所有实现意外地达成了一致,但规范并不要求这个一致。你在生产 JSON 时如果出现重复键,不要假定消费者看到的是你以为的那个值。你在解析 JSON 时如果输入有重复键,那个输入要么是有 bug,要么是恶意构造的,应该拒绝。

这也是一个已知的安全向量:如果请求校验器和请求处理器是不同的解析器,攻击者可以构造 {"role": "user", "role": "admin"},让一个解析器看到 "user",让另一个看到 "admin"。这类 CVE 真实存在。

转义表

JSON 字符串内必须转义的字符只有这些

必须转义 原因
" 否则会闭合字符串
\ 它是转义起始符
控制字符 U+0000 到 U+001F 规范要求

加上六个具名转义:\b \f \n \r \t \"(以及 \\),还有 \uXXXX 用于任意 Unicode 码点。\/ 是允许的但永不强制——你的解析器为了 HTML 嵌入安全可能会输出它,但规范不要求。JSON 也沿用了 JavaScript 的 UTF-16 代理对设计选择:BMP 之外的字符必须以高位 / 低位代理对编码,比如 😀 写成 \uD83D\uDE00

值得注意:U+2028 与 U+2029(行分隔符与段分隔符)在 JSON 字符串里不需要转义,但在 ES2019 之前它们在 JavaScript 源码中是非法的。也就是说,2019 年之前如果你 eval JSON.parse 的输出、或者把 JSON 直接内联到 <script> 标签里,遇到这两个字符就会语法报错。(不要 eval JSON。但这就是为什么有些老代码会无端转义这两个字符。)

NDJSON、JSON Lines 与流式问题

标准 JSON 整体是一个值。要把一百万条记录作为一个 JSON 数组流式传输,你得先写 [,再写每一条记录,再写 ],消费方要么把整个文档读完再解析,要么用流式解析器。

务实变体:NDJSON / JSON Lines——每行一个合法 JSON 值,用 \n 分隔,没有外层数组。生产容易(循环里 print(json.dumps(record)) 即可),天然可流式(读一行解一行),可恢复(处理崩在第 4,000,000 行就从那里继续)。Elasticsearch 的批量 API、BigQuery 导入、OpenAI 的 batch API、各家日志收集器都用它。

文件名约定 .ndjson.jsonl。没有正式标准,但形态全行业一致。

JSON5、JSONC、HJSON:你其实想要的是 YAML

配置文件是 JSON 最糟糕的用例。没有注释、没有尾随逗号、没有多行字符串、严格双引号——这些每天都在产生摩擦。于是出现了若干扩展:

  • JSON5 —— 加注释、尾随逗号、单引号字符串、不带引号的键、十六进制数、首尾省略 0 的小数、+ / Infinity / NaN。Babel、ESLint 配置等使用。
  • JSONC(JSON with Comments)—— 微软的变体,只加 ///* 注释。VS Code 的 settings.jsontsconfig.json 用它。
  • HJSON —— 用空白做语义、最少标点的 JSON。小众。

每一个看起来像 JSON没有一个能用标准 JSON 解析器解。如果你发布一个以 .json 结尾、但要 JSONC 注释支持的文件,你就给下游工具(CI、部署脚本、语言服务器校验器)创造了一类 bug——它们会对你的编辑器看着完全合法的内容报"非法 JSON"。

实用规则:*.json 文件应该能被 JSON.parse 解。 解不了就改名 *.jsonc*.json5,并接受你已经走出了标准。

JSON Pointer、JSON Patch、JSON Schema:堆在 JSON 之上的那些东西

JSON 这个格式没有语义。JSON 这个生态花了二十年用一系列分层规范在格式上叠加语义:

  • JSON Pointer(RFC 6901)—— 用斜杠分隔的路径定位 JSON 文档里的单个节点:/items/0/name。像 XPath,但简单很多。
  • JSON Patch(RFC 6902)—— 用一组操作描述 diff:[{"op": "replace", "path": "/items/0/name", "value": "x"}]。Kubernetes、ETCD、一些 HTTP PATCH API 用它。
  • JSON Schema(当前为 draft-2020-12,几乎是真正的标准)—— 一份 JSON 文档描述另一份 JSON 文档的形态与校验规则。规范本身比 JSON 大得多;各实现合规度参差;关键字是有机增长出来的。有用,偶尔抓狂。
  • JSON-LD —— 加上语义化网络元数据(@context@id@type),让一个 JSON 对象可被解读为 RDF 三元组。Schema.org 结构化数据、ActivityPub、若干 W3C 规范用它。

你不需要懂这些就能用 JSON,但写 API 规范时会遇到。

常见坑

  • 大整数在 JavaScript 里丢精度。 ID > 2^53 用字符串发。
  • 重复键。 别生产,别假定消费方解析行为。
  • 从 JS 对象字面量带过来的尾随逗号。 JSON.parse('{"x":1,}') 直接抛。
  • 不带引号的键。 {x: 1} 是 JS 对象字面量,不是 JSON。JSON 要 "x"
  • 单引号。 同样:合法 JS、非法 JSON。
  • *.json 里写注释。 编辑器解得了,CI 解不了。
  • 生产端写出 NaN / Infinity。 JSON.stringify 写成 null;某些自定义序列化器写成字面量 NaN,是非法 JSON;消费端两种都翻车。序列化前清洗数字。
  • 混用编码。 JSON 字符串是 Unicode。UTF-8 是标准传输编码(RFC 8259 §8.1)。如果生产端是 Latin-1 而消费端是 UTF-8,那就是 bug。
  • 日期约定。 永远 RFC 3339 / ISO 8601 带时区,永远字符串,永远在 API 合同里写明。不要 Unix 毫秒,不要本地化日期。
  • JSON 里嵌 JSON 字符串。 一个字段值是另一份 JSON 序列化后的字符串,转义复杂度翻倍、类型安全归零。除非下游系统真的强制要求,否则别这么做。

什么时候该换格式

JSON 适合:HTTP API、结构化日志、用户不需要写注释的配置文件、任何需要跨语言文本格式的地方。

更好的选择:

  • 配置文件需要注释和尾随逗号 → TOML(万不得已 YAML,连同它的所有坑)。
  • 强 schema、性能敏感的消息 → Protobuf、Avro、Cap'n Proto、MessagePack。规模上来后 JSON 的文本开销与解析成本会显著。
  • 流式事件 → 必须保持 JSON 形态用 NDJSON,否则上 Avro container files 之类。
  • 散文 → Markdown。{"text": "..."} 当传输载体可以,当编辑载体糟糕。
  • 二进制 blob → 量大就别 Base64 进 JSON。用 multipart 格式或单独通道。

教训

Crockford 对"不加新特性"的纪律性,正是 JSON 成为通用语言的原因。每一个替代品——YAML、XML、INI、TOML、甚至 Protobuf——都比它表达力更强,也都比它装机量更小。胜出的取舍,2026 年和 2001 年一样:简单到每个解析器都达成一致、每种语言都有支持、每个开发者都已经会用。

你希望 JSON 拥有的那些特性——注释、尾随逗号、日期、整型——都可以在应用层用约定、schema 或姊妹格式补回来。协议层的简洁是不可让步的,整件事的全部价值就在这里。

如果你只能带走一条经验:不要和格式作对。需要 JSON 擅长的事,用 JSON;需要 JSON 拒绝给的特性,换工具;不要把 JSON 装不下精度的数据塞进 JSON。

主要参考资料

用于核对本文技术细节的标准与官方文档。

在浏览器里直接格式化与校验

本站 JSON 工具全部在浏览器内完成格式化、压缩与校验,错误信息直接来自 JavaScript 解析器并定位到行。适合在配置文件上线前做最后一道把关。数据不会离开浏览器。

打开 JSON 工具

相关文章

继续阅读同一主题领域的实践指南。

查看全部文章

Cookie 同意

我们使用 Cookie 来增强您的体验并展示相关广告。您可以自定义您的偏好。