为什么 JSON 成了默认选项
JSON 能赢不是因为它设计得最优雅,而是因为它在「够用」和「简单」之间卡在了正确的位置。对比一下三个主流交换格式:
| 维度 | JSON | XML | YAML |
|---|---|---|---|
| 体积 | 小(无冗余标签) | 大(成对标签) | 最小 |
| 数据类型 | 6 种,够用 | 需 XSD 定义 | 丰富,含类型推断 |
| 注释 | ❌ 不支持 | ✅ | ✅ |
| 解析成本 | 极低(语言原生) | 高(DOM/SAX) | 中(缩进敏感) |
| 人写友好度 | 中(括号易错) | 低 | 高 |
| 主要地盘 | Web API、配置、日志 | 企业/金融遗留系统 | K8s、CI、IaC |
结论:机器与机器之间高频交换用 JSON,人手工维护的配置文件用 YAML。这也是为什么 Kubernetes 的 manifests 是 YAML,而它的 API 通信是 JSON。
需要随时对照数据类型与转义规则时,本站的 JSON 速查表 可以直接开在旁边。
JSON 语法:严格在哪里
JSON 的全部语法规则加起来只有几条,但每一条都是硬性的,没有容错空间。
六条硬规则
- 键名必须用双引号:
"key",不能是key或'key' - 字符串值必须用双引号,不能用单引号或反引号
- 不允许尾随逗号:最后一个元素后面不能留逗号
- 不支持注释:
//和/* */都会导致解析失败 - 必须使用 UTF-8 编码(RFC 8259 的强制要求)
- 顶层可以是对象、数组、字符串、数字、
true/false/null——不限于对象
六种数据类型
| 类型 | 示例 | 注意点 |
|---|---|---|
| string | "hello" |
必须双引号;控制字符要转义 |
| number | 42、3.14、1e10 |
不支持 NaN、Infinity、前导 0、十六进制 |
| boolean | true / false |
只能小写 |
| object | {"a": 1} |
键必须是字符串 |
| array | [1, 2, 3] |
元素类型可混 |
| null | null |
不是 undefined、不是 NULL |
合法 / 非法对照
| 写法 | 判定 | 原因 |
|---|---|---|
{"a": 1} |
✅ | — |
{a: 1} |
❌ | 键名未加引号 |
{'a': 1} |
❌ | 单引号 |
{"a": 1,} |
❌ | 尾随逗号 |
{"a": 01} |
❌ | 前导零 |
{"a": NaN} |
❌ | JSON 无此字面量 |
{"a": undefined} |
❌ | 无此类型 |
[1, "2", null] |
✅ | 数组元素可混类型 |
格式化与压缩:不是审美问题
压缩能省多少
去掉所有缩进与换行,实测通常减少 30–50% 体积;如果键名重复度高(如一万条同类记录),配合 gzip/Brotli 后差距会进一步拉大。反过来,一个 10MB 的 JSON 压缩后能省 3–5MB 传输量——在移动端弱网环境下这就是几秒的差距。
需要压的时候直接用 JSON 压缩工具,比手写正则删空白可靠(正则会误伤字符串内部的空格)。
缩进约定
| 风格 | 使用场景 |
|---|---|
| 2 空格 | 最主流,JS/TS 生态默认(Prettier、ESLint) |
| 4 空格 | Python 生态、部分 Java 项目 |
| Tab | 少见,但与 YAML 不兼容,不推荐 |
关键是同一仓库内保持一致,差异比选择本身更伤。
两个反直觉的注意点
- 格式化之后再签名会验签失败。HMAC、数字签名、JWT 的计算对象是原始字节序列,多一个空格结果就完全不同。
- 生产日志不要打印格式化后的大对象。应打印压缩版,并按长度截断,否则日志体积和写入耗时都会失控。
验证:把错误挡在运行时之前
JSON.parse 抛出的错误往往只有一句 Unexpected token } in JSON at position 1234,看起来毫无信息量。掌握分布就能秒定位。
五类高频错误
| 错误类型 | 典型报错 | 真实原因 |
|---|---|---|
| 尾随逗号 | Unexpected token } |
对象/数组最后一项后多写了逗号 |
| 单引号 | Unexpected token ' |
从 JS 字面量复制过来 |
| 括号不闭合 | Unexpected end of JSON input |
少了 } 或 ],报在文件末尾 |
| 注释 | Unexpected token / |
手写了 // 说明 |
| BOM 头 | Unexpected token 在 position 0 |
文件以 开头 |
定位心法
报错位置往前看,不要往后看。 解析器是在遇到「无法继续的字符」时才报错的,而笔误通常在它前面几行。具体到症状:
- 报错在末尾 → 几乎一定是括号不闭合,从外往内数配对
- 报错指向逗号 → 通常是多写了尾随逗号,而不是少写内容
- 报错在 position 0 → 先查 BOM 和编码
- 报错在中间某行 → 看这一行的上一行末尾
把原始内容粘进 JSON 格式化工具 是最快的一步,多数实现会直接标出出错行列号。
JSON Schema:把结构变成可执行的契约
验证「是不是合法 JSON」只解决了语法问题;验证「字段对不对、类型对不对」要靠 JSON Schema。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["id", "email", "createdAt"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "format": "uuid" },
"email": { "type": "string", "format": "email" },
"age": { "type": "integer", "minimum": 0, "maximum": 150 },
"status": { "enum": ["active", "suspended", "deleted"] },
"createdAt": { "type": "string", "format": "date-time" }
}
}
常用关键字
| 关键字 | 作用 |
|---|---|
type |
类型约束,可给数组如 ["string", "null"] |
required |
必填字段列表 |
enum |
枚举允许值 |
minimum / maximum |
数值范围 |
minLength / maxLength |
字符串长度 |
pattern |
正则匹配(配合 正则测试工具 调试) |
additionalProperties |
设为 false 可拒绝未声明字段 |
format |
语义格式(email/uri/uuid/date-time 等) |
从真实响应反推 schema
手写 schema 容易和真实数据漂移。更稳的做法是拿一份真实响应体生成初版,再人工收紧约束——JSON Schema 生成工具 就是干这个的。生成后把 additionalProperties 收紧、把可以为空的字段标注清楚,才算一份能用的契约。
在 CI 里用 ajv 跑一遍,API 改动导致的结构破坏会在合并前暴露。
大文件与流式处理
内存不是「文件多大就占多大」
这是最容易误判的一点:JSON 解析后在内存里的占用通常是原始文本的 3–10 倍。因为每个键和值都会变成独立对象/字符串,附带指针与元数据开销。一个 100MB 的 JSON 文件,直接 JSON.parse 很可能需要 500MB–1GB 内存。
| 文件规模 | 建议做法 |
|---|---|
| < 10 MB | 直接 JSON.parse,不必优化 |
| 10–100 MB | 流式解析,或按批次切片处理 |
| > 100 MB | 必须流式,且考虑换行分隔的 JSONL 格式 |
| > 1 GB | 改用专门的列式/二进制格式(Parquet、Arrow) |
流式解析器
| 库 | 语言 | 特点 |
|---|---|---|
simdjson |
C++ / 多语言绑定 | 利用 SIMD 指令,解析可达 GB/s 级 |
ijson |
Python | 惰性迭代,边读边产出 |
JSONStream |
Node.js | 按 JSONPath 增量抽取 |
serde_json (StreamDeserializer) |
Rust | 零拷贝友好 |
抽取字段用 JSONPath
如果只需要大文件里的某几个字段,不必解析整棵树——用 JSONPath 表达式直接定位,边解析边丢弃无关分支。表达式写法可以在 JSONPath 查询工具 上对着真实数据调。
结构对比:改完之后到底变了什么
改配置、升级接口之后,肉眼比对两个 JSON 几乎必然出错。正确做法是对比解析后的结构而非文本:
- 键的增删 → 关注
required与兼容性 - 值的变化 → 关注类型是否漂移(
"1"与1) - 数组顺序变化 → 判断业务上是否有序语义
JSON Diff 给的是结构化差异,比 diff 命令的文本行差异可靠得多(后者会被格式化差异淹没)。
安全:解析 JSON 的四个真实风险
| 风险 | 危害 | 修法 |
|---|---|---|
eval() / new Function() 解析 |
远程代码执行,直接沦陷 | 一律用 JSON.parse |
| 原型链污染 | 深合并后篡改所有对象原型 | 合并前剔除 __proto__/constructor/prototype |
| 深度嵌套爆栈 | 递归解析器栈溢出,服务拒绝 | 解析前做深度上限校验 |
| 大整数精度丢失 | 金额/订单号静默变成错误值 | 超过 2^53-1 的字段用字符串传输 |
第 2 条最隐蔽:
const payload = JSON.parse('{"__proto__":{"isAdmin":true}}');
const config = {};
merge(config, payload); // 危险:污染 Object.prototype
console.log({}.isAdmin); // true —— 全站对象都被改了
第 4 条在金融、订单类接口里最致命:9007199254740993 解析后变成 9007199254740992,且不会报错。
常见任务速查表
| 任务 | 做法 | 工具 |
|---|---|---|
| 读懂压缩成一行的 JSON | 格式化(2 空格缩进) | JSON 格式化 |
| 排查解析报错 | 粘进去看行列号 | JSON 格式化 |
| 缩小传输体积 | 压缩去空白 | JSON 压缩 |
| 折叠浏览深层结构 | 树形查看器 | JSON 查看器 |
| 对比两版配置差异 | 结构化 diff | JSON Diff |
| 从响应反推 schema | 自动生成 + 人工收紧 | JSON Schema 生成 |
| 从大文件抽字段 | JSONPath 表达式 | JSONPath 查询 |
| 调试字段级正则 | 先验证 pattern | 正则测试 |