ToolAct工具行动
什么是 JSON?

什么是 JSON?

生活实用2026年8月12日15 分钟阅读

从一次网络请求说起

先讲一个我经常看到的场景。

你打开浏览器开发者工具的 Network 面板,刷新页面,点开一条 XHR 请求,切到 Response 标签页。等待你的是一大坨密密麻麻的文本——没有换行、没有缩进,一眼望去全是花括号、引号和冒号。第一次见到它的人,多半会觉得这是乱码。

但它不是乱码。它是服务器在跟你说话,用一种双方事先约定好的语言,把一个用户、他的订单、他的支付状态,原封不动地打包扔过来。前端拿到的第一反应通常是 JSON.parse(resp),这一坨文本就变回了真正的数据结构。我入行那会儿第一次看懂这个瞬间,还挺震撼的:明明是一串字符,任何一门语言却都能读懂它。

后来我慢慢明白,这就是 JSON 最大的本事。它不是给某家公司、某种语言专用的私有格式,而是全世界不同系统在说"我们交换一下数据吧"时,默认会说的那门方言。大到云厂商的 SDK,小到你电脑里某个工具的配置文件,都在用它。

JSON 到底是什么

JSON,全称 JavaScript Object Notation,直译过来是"JavaScript 对象表示法"。

它诞生于 2001 年前后,作者是 Douglas Crockford。流传的版本大致是:他在给一个项目找一种比 XML 更轻、更容易解析的数据交换格式,翻来覆去发现 JavaScript 里直接写对象字面量的那套语法就挺好用,干脆把它单独抽出来,定成一小套规范,放上了 json.org。后来还有一段轶事,说他把"JSON"这个名字连同它的使用许可以"让它永远属于全人类"的姿态免费送了出去。真假先不论,反正这个名字从此传开了。

所以它的语法血缘来自 JavaScript 的对象字面量:花括号、冒号、逗号、双引号,都是 JavaScript 里写对象的写法。但"JavaScript 对象表示法"这个名字确实起得有点误导人。除了语法血缘,它跟 JavaScript 一点关系都没有——JSON 不依赖任何 JavaScript 运行时,C、Java、Python、Go、Rust,全都能解析和生成它。说白了,它就是一门"碰巧长得像 JavaScript 对象"的纯文本格式。Crockford 自己都调侃说这名字起错了,不如叫 JOSN——Just Old Simple Notation。

它的标准化过程也挺曲折。2013 年 ECMA 发布了 ECMA-404,2017 年 IETF 发布了 RFC 8259,取代了之前的 RFC 7159,往前还能追到 2013 年的 RFC 7158 和 2006 年的 RFC 4627。规范本身薄得可怜,RFC 8259 全文才几千词,比很多公司内部的技术方案还短。这其实侧面说明了一件事:JSON 能流行,恰恰是因为它简单到没什么可写。

为什么是它赢了 XML

要理解 JSON 为什么赢,得先回忆一下 JSON 出现之前的世界。

那是 XML 的时代。SOAP 做 Web 服务,一个请求报文能写好几页,拆开一看是 <soap:Envelope><soap:Body>,里面再嵌套各种带命名空间的元素。配置文件也写成了通讯录的样子:每个设置项都要包好几层标签,你读半天才能找到"哦,端口是 8080"。更别提还有属性(attribute)和元素(element)之争——<width unit="px">100</width> 还是 <width><unit>px</unit><value>100</value></width>?社区吵了好几年,没有定论。再往上还有命名空间的坑,以及 XSLT——一门专门为了把 XML 转换成别的格式(比如 HTML)而发明的语言,你想把 XML 变成可读的文档,还得先学会它。

JSON 不是被设计出来打败 XML 的。它就是有人看着这堆东西觉得烦,然后问了一句:我们能不能别这么折腾?

能。而且答案简单得过分:

  • 数据是字典,就写 {}
  • 数据是列表,就写 []
  • 字符串、数字、布尔、空,直接写。

每种语言里早就有对应的数据结构了——Python 的 dict 和 list,Java 的 Map 和 List,Go 的 map 和 slice,JavaScript 的对象和数组。JSON 是跟它们一比一对应的,不需要 schema,不需要事先声明,看见就懂。学习曲线近乎为零,一个下午就能上手。解析器的实现也简单到能在周末自己写一个。

所以不是 JSON 写得多漂亮。JSON 的语法谈不上好看,它只是不怎么碍事。它赢的方式很朴素:不是它足够好,而是别的东西实在太烦人了。不是 JSON 打败了 XML,而是天下苦 XML 久矣。

六种数据类型

JSON 一共只有六种数据类型,一只手加一根手指就数得完。

对象(object):花括号包起来的键值对集合,键必须是字符串,值可以是任意类型。

{"name": "张三", "age": 28}

数组(array):方括号包起来的有序列表,元素之间用逗号分隔,元素类型可以混着放。

["北京", "上海", "广州", "深圳"]

字符串(string):必须用双引号包起来,特殊字符用反斜杠转义。注意 JSON 里没有单引号字符串,'hello' 不是合法的 JSON。

"你好,世界"

数字(number):整数、小数、科学计数法都行,但不能有前导零,08 是非法数字;也没有 NaN 和 Infinity,这两个 JavaScript 里存在的东西,JSON 里没有。

42, -3.14, 6.02e23

布尔值(boolean):只有 truefalse 两个值,必须全小写。TrueTRUEFALSE 全都不对。

空值(null):表示"什么都没有"。注意它跟 0、空字符串、空数组都不是一回事。

把这六种拼起来,就是一个真实的记录。比如一个用户:

{
  "id": 1024,
  "name": "李雷",
  "avatar": null,
  "tags": ["前端", "全栈"],
  "active": true,
  "profile": {
    "city": "杭州",
    "github": "https://github.com/lilei",
    "followers": 4823
  }
}

嵌套多少层都行。对象套数组,数组里再套对象,任何真实的 JSON 报文,都是这六种类型以不同深度组合出来的。学会这六种,你就学会 JSON 的全部语法了。

那些容易踩的语法坑

JSON 语法简单,但简单不等于不会出错。恰恰因为它简单,出错往往出在最不起眼的地方。

第一,键必须用双引号。{name: "张三"} 在 JavaScript 里能跑,在 JSON 里是错的,必须是 {"name": "张三"}。很多人第一次手写 JSON 就栽在这,因为 JS 的对象字面量允许不带引号的键。

第二,字符串只能用双引号。"it's ok" 合法,'it's ok' 非法。字符串内部的引号要么转义,要么用另一种引号绕开,但外层永远是双引号。

第三,没有注释。想加一行"这里别动,改了构建会挂"?不行。设计上就是故意不给,因为一旦允许注释,数据文件就慢慢变成程序文件了。这个矛盾在配置文件场景里反复发作,后面聊 JSON5 时会再碰到。

第四,不能有尾随逗号。数组或对象最后一项后面多一个逗号,解析器直接报错。这是它跟 JavaScript 数组最大的差异之一——ES 早就允许尾逗号了,JSON 到现在都不许。

第五,数字不能有前导零,0123 非法。-0 倒是能解析,但语义含糊,很多语言读出来就是 0

第六,重复的键是合法的。{"a": 1, "a": 2} 不会报错,但规范明确说"行为未定义"。绝大多数解析器的做法是后者覆盖前者,取 2。可别依赖这个行为写业务逻辑。

第七,转义。JSON 支持一套完整的转义集合:\"\\\/\b\f\n\r\t\uXXXX。其中 \uXXXX 是 Unicode 码点,比如 \u4f60 就是「你」。遇到 emoji 这类超出基本多语言平面(BMP)的字符,一个码点要拆成两个 \u 代理项拼起来,比如 \ud83d\ude00 就是 😀。好在这些很少需要手写,序列化库都帮你处理了。

JSON 是 JavaScript 的子集吗

网上有个流传很广的说法:JSON 是 JavaScript 的子集。这话半对半错,值得掰开揉碎讲清楚。

RFC 4627(2006 年那版)确实把 JSON 定义为 JavaScript 的一个子集。但后来这个说法被证明很尴尬。最典型的反例是 U+2028 和 U+2029——行分隔符和段落分隔符。这两个字符在 JSON 的字符串里允许直接出现,但在当时的 JavaScript 里它们是行终结符,会让字符串字面量直接断掉。也就是说,一份完全合法的 JSON 文本,放进当时的 JavaScript 里可能是非法代码。直到 ES2019 才把这两个字符纳入字符串字面量的合法范围。但这桩公案也说明,"子集"这种说法早就被各种边角差异戳穿了,所以后来的 RFC 8259 不再宣称"子集",只谨慎地说 JSON"派生自"JavaScript。

更重要的区别在于,JSON 跟 JavaScript 的对象字面量根本不是一回事。JavaScript 的对象字面量允许单引号字符串、尾随逗号、注释、函数、undefined、NaN、Infinity、Date 对象……这些 JSON 统统没有。你写 {a: 1, b: function(){}} 在 JS 里跑得好好的,但它绝不是 JSON。

这个区别在工程上很要命,因为它直接决定了你怎么解析。永远不要想拿 eval 或者 new Function 去解析 JSON——那是在把数据当代码执行,后果在安全那一节细说。要用真正的解析器。JavaScript 里的 JSON.parse 是严格按 RFC 实现的,多一个逗号、少一个引号、多一个尾逗号,一律拒绝执行。这正是"长得像"和"就是"之间的那条红线。

每天都会遇到的场景

JSON 之所以"无处不在",是因为它出现在太多你平时不会特意留意的角落。

REST API 是最典型的。前后端交互的请求体和响应体,绝大多数就是 JSON。你调一个天气接口,返回 {"city": "北京", "temp": 32, "humidity": 65},前端 response.json() 一拿就能用。GraphQL 的响应也是 JSON,很多 SDK 的请求参数也让你直接传 JSON 对象。

配置文件。package.json 是 JSON,tsconfig.json 是 JSON,ESLint 的 .eslintrc 早年也是 JSON(后来社区嫌它不能写注释,才流行起 .eslintrc.js)。无数工具的配置都是 JSON 格式,你天天在跟它打交道,只是未必意识到。

浏览器存储。localStorage 只能存字符串,所以存对象之前得 JSON.stringify,取出来再 JSON.parse。这一进一出,就是无数前端面试题的出题原型。IndexedDB 虽然面向对象,但很多封装库底层也离不开 JSON 序列化。

数据库。PostgreSQL 有 jsonb 类型,可以直接把 JSON 存进去并建索引查询。MongoDB 存文档用的 BSON,本质上是 JSON 的二进制变体。不同数据库之间导数据,中间格式往往也是 JSON。

流式场景用 JSON Lines,也叫 NDJSON。每一行是一条独立、完整的 JSON 数据(实践中通常是一个对象),行和行之间没有关系。日志、数据管道、机器学习训练数据,全用它。为什么?因为它对流式处理极度友好——不用等整个文件读完,读一行处理一行,内存占用小,还能断点续传。

WebSocket 的消息体。服务端往浏览器推一条消息,很多时候就是一个 JSON 字符串。心跳、推送、实时协作,都是它。

还有浏览器的 fetchfetch(url).then(r => r.json()) 大概是过去十年被写下次数最多的前端代码之一。说真的,今天很难找到一条完全不碰 JSON 的数据链路。

各种语言怎么解析

每个主流语言都有解析 JSON 的标准库或事实标准库,下面只是走马观花,不是教程。

JavaScript 不用说,JSON.parseJSON.stringify 是一对黄金搭档,一个把字符串变对象,一个把对象变字符串。有个坑很常见:JSON.stringify 会静默跳过对象里的 undefined、函数和 Symbol,字段悄悄就没了。

Python 用 json 模块,json.loads(s)json.dumps(obj) 一一对应。dumps 默认会把中文转成 \uXXXX 的转义形式,嫌日志里难看就传 ensure_ascii=False。读写文件对应的是 json.loadjson.dump

Java 生态的事实标准是 Jackson 和 Gson。它们的核心卖点是能把 JSON 直接映射成你定义好的 POJO:字段名对上就自动填进去。代价是反射慢、配置多,@JsonProperty@JsonIgnoreProperties 一堆注解砸下来,新人上手容易懵。

Go 的 encoding/json 用结构体标签声明映射:json:"fieldName"。它的脾气很特别:字段没写 omitempty 标签时,零值也会被序列化出去。另一个经典坑是 json.Unmarshal 默认把 JSON 里的数字解析成 float64,大整数精度直接丢。

Rust 生态最常用的是 serde,配合 serde_json。它靠 derive 宏在编译期生成序列化代码,#[derive(Serialize, Deserialize)] 往结构体上一放,它就"会"序列化了。性能好、类型安全,代价是学习曲线陡——当然,Rust 的所有东西学习曲线都陡。

不管哪种语言,思路都一致:把 JSON 文本解析成内存里的数据结构,用完再序列化回去。差别只在类型系统、性能细节和 API 设计上。

安全:那些用血换来的教训

JSON 的安全史,是一部用事故当教材写出来的历史。

最著名的要数 eval 灾难。早期没有 JSON.parse,很多人图省事,直接 eval('(' + json + ')'),把 JSON 当 JavaScript 代码执行。这等于把服务器的输出当程序跑——如果数据里混进了恶意片段,比如字符串里藏着 )};alert('xss')//,浏览器就真的会去执行它。轻则 XSS,重则任意代码执行。这个问题严重到所有正经解析器后来都明令禁止 eval。

然后是 JSONP。跨域问题还没解决的年代,<script> 标签不受同源策略限制,于是有人把数据包成一个回调函数调用:callback({"data": 1}),前端定义好 window.callback 等它执行。它能绕过跨域限制,但代价很实在:一是只能发 GET,二是你把执行权完全交给了对方返回的内容——它返回的不是数据,是代码。CSRF 和 XSS 风险都摆在那。

还有一个直到今天仍在影响我们的陈年旧账:顶层数组。早年 JSON 报文允许整个文档就是一个数组,[1,2,3]。这在普通场景下没问题,但它给一种叫 JSON 劫持(JSON Hijacking)的攻击开了口子——老浏览器允许覆盖 Array 构造函数和它的原型方法,攻击者让 <script> 去加载受害站点的顶层数组,页面里的 Array.prototype 一旦被改写过,就能在脚本执行时把数组里的数据顺走。当年 Google 是怎么修的?把响应包成对象,{"data": [...]}。这个"敏感接口别返回顶层数组"的约定流传至今,你翻老代码会发现很多接口至今守规矩地在最外层包一个对象。讽刺的是,后来的 RFC 7159 把顶层允许的类型放宽到任意值,也就是说"顶层数组"在标准层面是合法的——但安全实践的惯性,比标准活得久。

再往后是原型污染(prototype pollution)。某些解析器或库在合并对象时会走 Object.assign 之类的逻辑,如果 JSON 里塞了一个 {"__proto__": {"isAdmin": true}},而库又把它直接并进原型链,你的对象就会凭空多出 isAdmin 属性,而且值还是真的。这类漏洞在 lodash 的 merge 上出过不止一次 CVE。

还有炸弹。一个合法但极深的嵌套 JSON,比如一万层 [[[[...]]]],能让递归解析器直接栈溢出崩溃;一个 {"a": {"a": {"a": ...}}} 反复嵌套能撑爆内存。这叫 JSON 炸弹,很多框架默认不限制解析深度和体积,就是潜在的隐患。

现代的最佳实践一句话就能说清:永远用真正的解析器,永远不要 eval;对外部输入限一下大小和深度;慎用那些会把数据合并进原型链的库。这四条,每一条背后都有真实的事故。

校验与扩展:Schema、JSON5

JSON 没有内置的约束能力——它就是一堆文本,字段名拼错了、类型写错了,规范层面管不着。所以社区造出了 JSON Schema:用一份 JSON 来描述另一份 JSON 长什么样。

{
  "type": "object",
  "required": ["name", "age"],
  "properties": {
    "name": {"type": "string", "minLength": 1},
    "age": {"type": "integer", "minimum": 0}
  }
}

你写一份这样的 schema,再用 ajv(JavaScript 生态里最常用的校验库)之类的工具,就能在运行期校验数据:缺字段报错、类型不对报错、超出范围报错。API 网关、表单校验、写库前的防线,都用得上。它本身没有走完正式的标准流程(IETF 有过草案但一直没转正),目前常用的是 2020-12 这一版。说句公道话,它挺啰嗦,写起来并不轻松,但它是目前唯一能跟 JSON 自洽的"类型系统"。

而 JSON 语法本身,因为缺注释、缺尾逗号,被不少人视为"配置格式里的二等公民"。于是各路补丁就来了。

JSON5,名字起得直白,就是"JSON 第 5 版(其实不是标准)"。它允许不加引号的键、单引号字符串、尾随逗号、注释,还引入了 InfinityNaN-0。很多前端配置用它写起来确实舒服,但注意它跟 JSON 不兼容,不能直接喂给 JSON.parse,得用专门的解析器。

JSONC,也就是"带注释的 JSON"。基本就是 JSON 加上注释,再放开尾随逗号。VS Code 的不少配置文件(比如允许写注释的 settings.json)用的就是 JSONC。但 VS Code 官方也提醒过:它不是一个正式标准。

HJSON,更激进的变体,目标是"让人类更容易写",支持无引号键、多行字符串、注释。受众更小。

这些都不是标准,是实用主义的补丁:因为 JSON 标准不肯加注释,大家就各自加。补丁一多,互不兼容的方言就多了。这大概也是"简单格式"的必然命运——太简单,就总有人想给它加东西。

它的局限与槽点

说到槽点,JSON 的局限还真不少。

没有注释。对数据交换格式来说无伤大雅,对配置文件来说就是折磨。package.json 里没法写"这行别动,改了构建会挂"。于是催生了 JSONC、JSON5,甚至有人干脆把配置写进 .js 文件里导出。

没有日期类型。这是最著名的坑之一。RFC 里没有日期,大家只好自己发明约定——主流是 ISO 8601 字符串,"2024-08-12T15:30:00+08:00"。于是每个人都得写一遍解析逻辑,或者引入 date-fns、Moment.js、Java 的 Joda-Time。二十多年了,日期在 JSON 里到底怎么表示,始终没有统一答案。每个团队都在重新发明同一个轮子。

数字精度。JSON 的数字是 IEEE-754 双精度浮点,能精确表示的整数上限是 2^53 - 1,也就是大约 9007199254740991。超过这个范围就开始出问题。银行、支付、记账系统,一个 19 位的雪花 ID,或者一笔大金额,JSON 走一圈再回来,精度可能就悄悄丢了。所以业界才有"金额用字符串传输"的约定,{"amount": "599.90"} 而不是 {"amount": 599.9}

没有二进制。要传一张图片、一个文件,只能 base64 编码成字符串,体积膨胀约三分之一。小文件无所谓,大文件就肉疼。

深层嵌套。JSON 本身没有深度上限,但绝大多数解析器是递归实现,几百层没事,几万层可能直接栈溢出。前面安全节说的 JSON 炸弹,利用的就是这个。

还有重复键行为未定义、null 和缺失字段在某些语言里分不清。这些也都算老熟人了。

说白了,JSON 的每一项局限,都是"简单"这个选择的另一面。你不可能既要它简单,又要它完备。要完备,就得拿复杂度来换。

替代品们

槽点这么多,自然有人造替代品。诚实地说,各有各的适用场景,也各有各的坑。

YAML。对人类最友好,缩进即结构,支持注释、锚点、多行字符串。但它"臭名昭著"的地方在于:缩进错一个空格,解析结果就跟你以为的完全不是一回事;onyesno 在某些实现里会被当成布尔值;大文件解析又慢又占内存。它是配置文件领域的王者(Docker Compose、GitHub Actions、Kubernetes),但拿它做数据交换,纯属给自己找事。常有人说:YAML 看起来没问题,用起来到处是坑。

TOML。为配置而生。方括号表示表,= 赋值,支持注释和日期类型,歧义少,解析器好写。Cargo.tomlpyproject.toml 都是它。但它只适合配置,不适合数据交换——没有 null,类型也偏弱。

Protocol Buffers。二进制、强类型,需要 .proto 文件定义 schema,再用工具生成各语言的代码。序列化体积小、速度快,是 gRPC 内部通信的默认序列化格式。代价是门槛高:前后端要先对好 schema,改字段还要小心版本兼容。它解决的是性能和类型安全,不是人可读。

MessagePack 和 CBOR。都是"把 JSON 变二进制"的思路,保留 JSON 的数据模型(对象、数组、数字……),但编码成紧凑的二进制。MessagePack 常被比作"二进制的 JSON",比 JSON 小,又比 Protocol Buffers 灵活——不需要 schema。CBOR 有标准化加持(RFC 7049),物联网场景常看到它。它们的共同代价是:人不可读,调试得靠工具。

BSON。MongoDB 在用,在 JSON 模型上加了日期、二进制、Decimal128 等类型,牺牲一点空间效率换性能。离开 MongoDB 生态基本没人用。

怎么选?我的经验是一句话:要数据交换、要人可读、要跨语言,用 JSON;纯配置且写给人看,用 TOML 或 YAML;性能敏感的内部 RPC,上 Protobuf;要体积小又要 JSON 的灵活,试试 MessagePack。别一上来就否定 JSON,多数情况下它已经够用了。

结语:无聊的格式才活得久

JSON 活了二十多年,靠的不是惊艳,是"够用"。

它哪儿都不算最优:体积不如二进制格式,可读性不如 YAML,配置体验不如 TOML。但它够简单、够普遍、几乎所有主流语言都自带或事实标配支持,于是它成了那个"不用商量、拿来就用"的默认选项。你不需要问同事"你们那边支持 JSON 吗",因为答案是肯定的。这种确定性本身,就是巨大的生产力。

我也见过不少人折腾"更优雅的格式",折腾完发现还是得给 JSON 写兼容层,因为下游接它的系统不认别的。格式战争里,赢的往往不是最好的那个,而是最没门槛、最没脾气、最平庸的那个。无聊,恰恰是它的核心竞争力。

所以当你纠结"要不要上 YAML 当数据交换格式"、"要不要发明一个带类型的 JSON 变体"时,先想想:这个格式能带来多少价值,又需要多少系统陪着改?很多时候,答案是不值得。YAML 留给配置,Protobuf 留给内网,JSON 就留给它已经管了二十多年的那摊子事。

最后用一句程序员圈的调侃收尾:写一个 JSON 解析器很容易,难的是说服所有人别再造一个。