JSON 格式化 & 验证器

JSON.parse 性能:V8 如何处理大型负载

现代 V8(Node.js、Chrome、Deno 背后的引擎)在 JSON.parse 中并不使用通用的递归下降解析器。自 2019 年起,V8 内置了一个用 C++ 编写的专用 fast-path 解析器,它直接在 UTF-16 字符串上工作,在 young generation 中分配对象,并利用 transition tree(hidden class)让数组中的同级对象共享形状(shape)信息。对于 [{"id":1,"name":"a"},{"id":2,"name":"b"}, ...] 这样的负载,第一个之后的每个对象都会以相同的 hidden class 创建——因此后续的属性访问是 monomorphic 的,并且会被内联。 JSON.parse 的开销主要由三部分决定:(1) 字符串的分词与反转义,(2) 结果对象图带来的 GC 压力,(3) reviver 回调的成本。添加 reviver 函数是最大的性能陷阱:它迫使 V8 退回到缓慢的通用路径,并对树中的每个键都调用一次 JS 函数。在 10 MB 的负载上,差距通常是 4–8 倍。如果只需要转换少数几个字段,先解析,再自己遍历结果。 对于由同质对象组成的超大数组,V8 常常能胜过流式 JSON 库,因为 fast path 优化得实在太好。但一旦单个文档超过约 100 MB,JSON.parse 会同时把整个原始字符串和结果对象树保留在内存中,峰值 RSS 可能达到文件大小的 3–4 倍。此时应改用 simdjson、oboe.js 或 stream-json 之类的流式解析器,它们在产生 token 的同时发出事件,从不物化完整的树。 V8 还会在多次调用之间缓存解析器的分词缓冲区,因此在紧凑循环中对许多小负载调用 JSON.parse,比把同样的总字节量放进一次巨型调用中解析更快。如果线上传输格式由你掌控,优先选择 NDJSON(每行一个 JSON 对象)——它可以增量解析,能按行从错误中恢复,并且与简单的按行缓冲 IO 配合良好。
// SLOW: reviver kills the fast path
const data = JSON.parse(raw, (key, val) => {
  if (key === 'createdAt') return new Date(val);
  return val;
});

// FAST: parse first, walk later
const data = JSON.parse(raw);
function walk(o) {
  for (const k in o) {
    if (k === 'createdAt' && typeof o[k] === 'string') o[k] = new Date(o[k]);
    else if (o[k] && typeof o[k] === 'object') walk(o[k]);
  }
}
walk(data);

// FASTER for huge files: NDJSON
import readline from 'node:readline';
const rl = readline.createInterface({ input: fs.createReadStream('big.ndjson') });
for await (const line of rl) {
  const row = JSON.parse(line); // 1 record at a time
  process(row);
}

生产环境中你一定会遇到的 JSON 陷阱

JSON 看起来简单,而这正是它伤人的原因。RFC 8259 对语法有严格定义:数字只有 IEEE-754 双精度浮点一种,字面量只有 true / false / null,字符串必须是双引号包裹的 UTF-8,而且没有原生日期类型、没有注释、没有尾随逗号,也没有 Infinity / NaN。所有与 JSON 相关的“生产环境怪异 Bug”,都是这六条规则中的某一条被现实打破的结果。 NaN 和 Infinity 不是合法的 JSON 值。JavaScript 的 JSON.stringify 会悄悄把它们转换成 null,这意味着 { "p99": Infinity } 这样的指标会被序列化为 { "p99": null },而你的仪表盘则安静地显示零。解决办法是在序列化之前对这些值做钳制或特殊处理——永远不要信任往返转换。 Date 对象的往返是不对称的。JSON.stringify(new Date()) 会通过 Date.prototype.toJSON 方法输出 ISO-8601 字符串,但 JSON.parse 完全不知道那个字符串是日期——你拿回来的是字符串,而不是 Date。如果你的代码在一次网络往返之后调用 data.createdAt.getTime(),它会抛出异常。要么用已知的 schema(zod、valibot、ajv)包裹解析过程,要么在文档中明确说明:时间戳返回时是字符串。 BigInt 是硬错误:JSON.stringify(1n) 会抛出 TypeError: Do not know how to serialize a BigInt。以 BigInt 返回 64 位 ID 的数据库驱动(Postgres 的 bigint、MySQL 的 UNSIGNED BIGINT)会不断撞上这个问题。务实的解法是:(a) 在应用启动时定义 BigInt.prototype.toJSON = function() { return this.toString() },并接受接收方拿到的是字符串;(b) 使用 superjson 这类能保留类型的序列化器。 循环引用同样是 TypeError。从 DOM 树到带 parent 指针的图数据结构,任何这类对象都会让 JSON.stringify 爆炸。replacer 参数可以跳过已知的循环键(返回 undefined 即丢弃)。调试时,util.inspect 或 structuredClone(内部会检测循环)更加友好。 最后是键的顺序:规范说对象是无序的,但 V8 的 JSON.stringify 会先输出类整数键,其余按插入顺序输出。如果你的测试断言的是精确的 JSON 字符串,那你测试的其实是实现细节——应该改为对解析后的对象进行断言。
// NaN/Infinity silently become null
JSON.stringify({ p99: NaN, max: Infinity });
// => '{"p99":null,"max":null}'  (BUG: silent data loss)

// Date asymmetric round-trip
const o = { at: new Date() };
const back = JSON.parse(JSON.stringify(o));
back.at.getTime(); // TypeError: back.at.getTime is not a function

// BigInt fix at app boot
BigInt.prototype.toJSON = function () { return this.toString(); };

// Circular reference handler
function safeStringify(obj) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (k, v) => {
    if (typeof v === 'object' && v !== null) {
      if (seen.has(v)) return '[Circular]';
      seen.add(v);
    }
    return v;
  });
}

JSON vs JSON5 vs JSONC——选对方言

JSON 本身是被冻结的标准——RFC 8259 就是规范,它刻意省略了一切便于人类使用的特性。这种克制对线上传输格式极好,对手工编辑的配置文件却很糟糕,这正是两种非官方方言主导配置领域的原因。 JSONC(“JSON with comments”)是 VS Code、TypeScript 的 tsconfig.json 以及大多数 Microsoft 工具链采用的方言。它在 RFC 8259 之上恰好只增加两样东西:行注释(//)和块注释(/* */)。尾随逗号被 VS Code 的解析器所容忍,但不属于任何成文规范。JSONC 刻意保持极简——除去注释之外,每个 JSONC 文档都是 JSON 文档。在 TypeScript 项目中无需安装任何库:编译器自带宽容的解析器,用一个小正则去掉注释(注意字符串内的处理)就能得到合法 JSON。 JSON5 更有野心:它加入了单引号字符串、不加引号的 ECMAScript-5 标识符键、多行字符串、十六进制数、前置/后置小数点(.5、5.)、数字前的正号、NaN 和 Infinity,以及尾随逗号。代价是 JSON5 不再是 JSON 的子集——你不能把 JSON5 文档喂给 JSON.parse。把它用在以人为本、机器端可以使用 json5 npm 包的配置文件上;绝不要把它当作线上传输格式。 第三个竞争者 HJSON 连键乃至短字符串值的引号都省掉了。它比 JSON5 更宽松,但生态支持更窄。除非你的工具已经以它为标准,否则请避开。 经验法则:API 和进程间消息坚持使用 RFC 8259 严格 JSON,以获得最大兼容性(每种语言都有稳定的解析器)。真正由人阅读和编辑的编辑器与构建配置可以用 JSONC,低摩擦地获得注释能力。只有当你确实需要 JSON5 的丰富特性、且写入方和读取方都在你掌控之中时,才去用它。并且绝不要让 JSON5 文档泄漏进期望 JSON 的系统——这种不匹配会在最糟糕的时刻抛出令人费解的“Unexpected token”错误。
// JSON (RFC 8259) — strict
{
  "name": "app",
  "ports": [3000, 3001]
}

// JSONC — comments + (de facto) trailing commas
{
  // tsconfig.json style
  "compilerOptions": {
    "target": "ES2022", /* matches Node 18+ */
    "strict": true,
  },
}

// JSON5 — humans first
{
  name: 'app',          // unquoted key, single-quoted string
  port: 0xABCD,         // hex
  rate: .5,             // leading decimal
  retries: +Infinity,   // signed Infinity
  // multi-line string
  banner: "line1\
line2",
}
最后更新:

关于此工具

JSON 格式化工具解析原始 JSON 并以统一缩进重新输出,遇到无效输入会给出清晰错误。是阅读压缩 API 响应、查找缺失逗号、把整洁负载附到 Bug 单的最快方式。JSON 是 REST 和配置的通用语言,这是开发者的日常工具。

使用方法

  1. 把 JSON 粘贴到输入面板。
  2. 带有正确缩进的格式化结果会显示在右侧。
  3. 如果解析失败,错误信息会指出第一个问题所在的行和列。
  4. 使用 Minify 按钮去除空白,生成用于生产环境的负载。
  5. 点击 Copy 将结果复制到剪贴板,以便在其他地方使用。

常见用例

  • 阅读来自 Stripe、GitHub 或 Slack 的压缩 Webhook 负载。
  • 把 curl 响应美化后再分享到 Slack 讨论串。
  • 验证手工编辑过的 package.json 或 tsconfig.json 是否仍能正常解析。
  • 审查 OpenAPI / Swagger 文档的结构。
  • 将两个 JSON 负载以一致格式美化后进行比较。
  • 把大型配置文件嵌入环境变量之前先进行压缩。

常见问题

Q. 可以在 JSON 中使用注释吗?

A. 标准 JSON 不允许注释。需要注释时请使用 JSONC(VS Code 配置)、JSON5 或 YAML 等替代方案。若必须是纯 JSON,请在解析前去掉注释。

Q. 为什么解析器报“Unexpected token”?

A. 几乎总是逗号缺失或多余、键没加引号,或使用了单引号字符串。键和字符串值都必须使用双引号。

Q. 允许尾随逗号吗?

A. 标准 JSON 不允许。有些解析器(JSON5、JavaScript 对象字面量)允许,但 JSON.parse 和大多数 API 都会拒绝。

Q. 日期应该如何表示?

A. JSON 没有原生日期类型。请使用 ISO-8601 字符串(例如 2026-05-01T10:00:00Z)——这是事实上的约定。