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