JSON 工具箱

JSON5 与 JSONC:带注释的 JSON

结论:两个方言都在解决「人写的配置需要注释和尾逗号」,但语法野心与生态渗透完全不同,选错会卡住工具链。

入门约 10 分钟发布于 2026-10-30更新于 2026-10-30
JSON5 与 JSONC 带注释的 JSON 封面

结论:需要在配置文件里保留注释时,VS Code / TypeScript 生态优先选 JSONC,需要尾逗号、单引号等完整 JSON5 语法时选 JSON5;对外接口与缓存仍应输出标准 JSON。

标准 JSON 不允许注释,于是每个团队都偷偷写注释,直到某天构建失败。与其偷偷做,不如选个正经方言。

一、两种方言的动机不同

  • JSONC(JSON with Comments):就是标准 JSON 加上注释与尾逗号,语法几乎不变。它是 VS Code 生态的「事实标准」,settings.json、tsconfig.json、.eslintrc.json 都用它。
  • JSON5:野心更大,直接对齐 ES5.1 的对象字面量——单引号、无引号键名、十六进制、Infinity/NaN、行尾反斜杠续行、+ 号开头。

一句话区分:JSONC 只让人写着舒服,JSON5 让人把配置文件当 JavaScript 写。

二、语法差异对照表

特性 标准 JSON JSONC JSON5
注释 // /* */ ✗ ✓ ✓
尾逗号 ✗ ✓ ✓
单引号字符串 ✗ ✗ ✓
无引号键名 ✗ ✗ ✓
十六进制数字 ✗ ✗ ✓
前导/尾随小数点 .5 5. ✗ ✗ ✓
+ 号开头数字 ✗ ✗ ✓
Infinity / NaN ✗ ✗ ✓
行尾 \ 续行 ✗ ✗ ✓
多行字符串 ✗ ✗ ✓(续行实现)
顶层任意值 ✓ ✓ ✓

JSON5 的完整规范只有一页多,实现 json5 包长期由社区维护,浏览器与 Node 行为一致。

三、解析:两套 API,两种定位能力

3.1 JSON5:简单的全量解析

import JSON5 from 'json5';

const config = JSON5.parse(`
{
  // 服务端口
  port: 8080,
  host: '0.0.0.0',
  features: ['json5', 'jsonc',],
  retries: 0x10,
}
`);

console.log(config.port, config.retries); // 8080 16

JSON5.parse 直接返回普通对象,没有偏移量信息——因为它不面向编辑场景,只面向「读配置」。

3.2 JSONC:带偏移量的语法树

jsonc-parser 的价值不在解析,而在编辑。它提供三层 API:

API 作用 典型用途
parse 解析为带 offset/length 的树 语法高亮、折叠、定位
visit 深度遍历,回调里拿到节点路径 与 JSONPath 类似的巡检
modify + applyEdits 生成最小文本编辑,再应用到原文 保留注释改值
import { parseTree, findNodeAtLocation, modify, applyEdits } from 'jsonc-parser';

const text = `{
  // 端口配置
  "server": {
    "port": 8080 // 生产环境为 80
  }
}`;

const tree = parseTree(text);
const portNode = findNodeAtLocation(tree!, ['server', 'port']);
console.log(portNode?.offset, portNode?.length);

const edits = modify(text, ['server', 'port'], 3000, {
  formattingOptions: { insertSpaces: true, tabSize: 2 },
});
console.log(applyEdits(text, edits));

输出会保留原注释,只替换 8080 这三个字符:

{
  // 端口配置
  "server": {
    "port": 3000 // 生产环境为 80
  }
}

这正是「用正则改配置」做不到的事:正则不知道该改第几个 8080,也不知道字符串里的 8080 不该动。

3.3 错误信息要带偏移量

jsonc-parser 的错误码是枚举值,常见的有:

错误码 含义
InvalidSymbol 出现了不该出现的字符
PropertyNameExpected 期望键名
ColonExpected 期望冒号
CommaExpected 期望逗号
CloseBraceExpected 对象未闭合
UnexpectedEndOfComment 注释未闭合
InvalidEscapeCharacter 非法转义

把 offset 换算成行列后,编辑器才能准确地把波浪线画在出错位置。

四、什么时候用哪一种

场景 推荐 理由
对外 API 请求/响应体 标准 JSON 方言不被通用客户端支持
package.json、composer.json 标准 JSON 包管理器只认标准 JSON
tsconfig.json、.vscode/settings.json JSONC 生态默认,工具链原生支持
应用业务配置(有注释需求) JSONC 或 YAML 需要保留注释,且要能被工具读写
需要 Infinity、十六进制、复杂字面量 JSON5 语法表达力最强
数据交换、日志、缓存 行分隔或标准 JSON 需要流式与通用性

一句经验:只要这份文件会被「非人类工具」读取,就优先标准 JSON;只有确定只被自家代码读,才考虑方言。

五、方言带来的三类风险

5.1 门槛扩散

一个 JSON5 配置文件被另一个工具读取时,对方用 JSON.parse 直接崩。典型现象是「本地开发好好的,上了 Docker 就报错」——因为容器里跑的是另一套读取逻辑。

对策:在仓库里加一条 CI 检查,明确列出哪些文件允许方言,其余一律标准 JSON 校验。

5.2 类型推断的意外

JSON5 允许无引号键名与单引号,但不允许把日期、正则之类的字面量引入——它仍然是数据格式,不是 JS 求值。 曾有人以为 JSON5 能写 new Date(),结果解析失败。JSON5 不是 eval。

5.3 注释被静默丢弃

JSON5.stringify 与 JSON.stringify 都不保留注释。所以「读进来改一改再写回去」会丢掉全部注释。 需要保留注释时只能用 jsonc-parser 的编辑式 API,或改用支持注释保留的 YAML 库。

六、实践建议

  • [ ] 团队文档里列一张「文件类型 → 格式」对照表,避免各自发挥;
  • [ ] 应用配置读取统一封装一个 loadConfig(),内部决定用哪套解析器,业务代码不直接 JSON.parse;
  • [ ] 需要程序化修改带注释的配置时,用 modify + applyEdits,禁止整体重写;
  • [ ] 方言文件在 CI 里也要校验(JSONC 用 jsonc-parser,JSON5 用 JSON5.parse),否则语法错误只能在运行时暴露;
  • [ ] 对外接口、缓存键值、消息队列里的负载,一律标准 JSON。

常见问题

JSON5 和 JSONC 应该怎么选?

看运行环境而不是语法数量:VS Code、TypeScript、ESLint 这类生态默认读取 JSONC,选它兼容性最好;需要尾逗号、单引号、无引号键名或 Infinity 等 JSON5 特性时,才引入 JSON5 解析器。两者都不适合直接作为对外 API 的响应格式。

为什么 JSONC 不能直接交给标准 JSON 解析器?

JSONC 允许注释和尾逗号,而 JSON.parse 只接受 RFC 8259 标准 JSON,遇到注释或尾逗号会直接报错。读取 JSONC 必须使用 jsonc-parser 这类专用解析器,或先去掉注释再交给标准解析器。

是否可以把 JSONC 里的注释保留到最终产物?

可以,但要走编辑式 API。jsonc-parser 的 modify 与 applyEdits 能只改动目标节点并保留其余注释;先 parse 成对象再 stringify 会丢掉全部注释。