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 會丟掉全部註解。