從 RFC 8259 讀懂 JSON 語法邊界
結論:把 RFC 8259 的語法產生式翻譯成人話:哪些字元能直接出現、哪些必須轉義、規範在哪些地方刻意留白。

結論:排查 JSON 語法問題時先定位第一個非法字元,再判斷它屬於單引號、尾逗號、註解還是控制字元,不要從頭重讀整段;編碼層面則先確認 UTF-8 且無 BOM,超過 2^53 - 1 的整數改用字串承載。
規範的正文只有十幾頁,但每一句「MUST」「SHOULD」「MAY」背後都藏著一個曾經讓團隊通宵的線上問題。
一、三份文字的關係
學 JSON 常被三個編號繞暈:RFC 4627、RFC 7159、RFC 8259,再加上 ECMA-404。關係其實很清楚:
| 文字 | 時間 | 地位 | 關鍵差異 |
|---|---|---|---|
| RFC 4627 | 2006 | 已被取代 | 頂層必須是物件或陣列 |
| RFC 7159 | 2014 | 已被取代 | 頂層放寬為任意 JSON 值 |
| RFC 8259 | 2017.12 | 現行標準 | 明確 UTF-8、補充互操作性建議 |
| ECMA-404 | 2013,第二版 2017.12 | 語法規範 | 只定義語法,不涉及編碼與傳輸 |
讀哪一份? 只看 RFC 8259 就夠了,ECMA-404 與它的語法部分等價,區別在於 RFC 8259 還規定了「用 UTF-8 傳輸」和「實現者應當注意的互操作問題」。
二、六種值型別
規範的語法產生式可以濃縮成一張圖:
JSON-text = ws value ws
value = object / array / number / string / "true" / "false" / "null"
object = "{" [ member *( "," member ) ] "}"
member = string ws ":" ws value
array = "[" [ value *( "," value ) ] "]"
要點只有六個:
- 物件用花括號,成員之間用逗號,最後一個成員後面不允許逗號;
- 陣列用方括號,元素之間用逗號,同樣不允許尾逗號;
- 數字不允許前導
+、不允許前導零(01非法)、不允許只有小數點(1.非法)、不允許NaN與Infinity; - 字串必須用雙引號,單引號非法;
- 布林與 null 是小寫字面量,
True、NULL都非法; - 只允許空白字元
0x20(空格)、0x09(製表)、0x0A(換行)、0x0D(回車)出現在結構之間。
下面這段是合法 JSON,很多人第一次看到會以為錯了——因為它是頂層的裸字串:
"just a string"
RFC 7159 之後頂層確實可以是任意值。但介面裡仍建議只用物件或陣列,原因在第六節。
三、字串裡到底能直接放什麼
這是最容易被「我本地能跑」騙過去的部分。規範對字串的規定是:
- 允許直接出現的字元:
%x20-21、%x23-5B、%x5D-10FFFF; - 也就是說,雙引號
"(0x22)與反斜槓\(0x5C)必須轉義; - 所有小於 0x20 的控制字元必須轉義,包括換行、製表、回車;
- 可用的轉義序列只有
\"、\\、\/、\b、\f、\n、\r、\t、\uXXXX。
{
"ok": "可以直接寫中文、emoji 🎉 和全形符號",
"escaped": "換行寫在這裡\n製表\t完成",
"control": "\u0007 是響鈴,必須轉義",
"quote": "他說:\"別用單引號\""
}
\/ 是歷史遺留的轉義(防止 </script> 提前閉合),現代場景基本不需要,但解析器必須支援。
四、編碼:UTF-8、無 BOM
RFC 8259 第 8.1 節說了三件事:
- 用於交換的 JSON 文字必須用 UTF-8 編碼;
- 實現不得在文字開頭新增 BOM(
EF BB BF); - 解析器可以忽略 BOM 以提高健壯性。
現實中的兩種典型故障:
- Windows 記事本另存為「UTF-8 帶 BOM」,檔案被
JSON.parse拒絕,報Unexpected token; - 用 GBK 儲存的中文 JSON,在只按 UTF-8 解碼的鏈路上變成亂碼,但語法居然還能過,於是錯誤延後到業務層。
處理方式很樸素:讀檔案時先剝掉 BOM,再顯式按 UTF-8 解碼;寫入時確保不帶 BOM。
/**
* 去掉字串開頭的 UTF-8 BOM。
* @param text 原始文字
* @returns 不含 BOM 的文字
*/
function stripBom(text: string): string {
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
}
順帶提一句媒體型別:application/json 不再接受 charset 引數,寫了雖然不報錯,但屬於過時寫法。
五、規範刻意留白的三處
規範沒有把話說死的地方,正是實現差異的來源。
1. 數字的範圍與精度
RFC 8259 只說「本規範不限制數字的範圍與精度」,同時指出大多數實現用 IEEE 754 雙精度。
這意味著同一個 9223372036854775807,在 JavaScript 裡會變成 9223372036854776000,在 Java 的 long 裡卻精確。
結論:超過 2^53 - 1 的整數、需要精確到小數後多位的金額,一律用字串承載,並在 schema 裡寫清 pattern。
2. 重複鍵
規範原文是「物件中的名稱應當(SHOULD)唯一」,沒有規定重複時怎麼辦。
JSON.parse取最後一個;- 部分解析器取第一個;
- 有的庫直接報錯。
結論:不要寫出重複鍵,也不要在校驗器裡依賴某一種行為。構建期加一條「重複鍵檢測」比事後排查便宜得多。
3. 深度與長度上限
規範沒有規定巢狀深度上限,但所有實現都有。V8 在幾萬層巢狀時會拋 RangeError: Maximum call stack size exceeded。
服務端必須自己設限,這是安全要求而不是規範要求。
六、用反例檢驗理解
下面每一行都是非法 JSON,可以拿去餵給解析器,觀察錯誤資訊是否友好:
{ 'name': 'json' } // 單引號
{ name: "json" } // 鍵名未加引號
{"a": 1,} // 尾逗號
[1, 2, 3,] // 陣列尾逗號
{"a": 01} // 前導零
{"a": +1} // 前導加號
{"a": .5} // 缺少整數部分
{"a": NaN} // 非標準字面量
{"a": undefined} // 非 JSON 值
{"a": 1} // 註釋 // 註釋
一個有意思的邊界:{"a": 1} 後面跟註釋在 JSON5 裡合法,在 JSONC 裡合法,在標準 JSON 裡非法。
所以「配置檔案能跑、介面傳過去就報錯」幾乎總是方言問題。
七、落地檢查清單
- [ ] 所有對外傳輸的 JSON 使用
application/json,不帶charset; - [ ] 檔案讀寫鏈路剝 BOM、顯式 UTF-8;
- [ ] 超過
2^53的整數用字串,schema 裡標註; - [ ] 構建期檢測重複鍵;
- [ ] 服務端限制請求體大小與巢狀深度;
- [ ] 對外介面的頂層值使用物件或陣列,不用裸標量。
常見問題
如何快速判斷一段文字符不符合 JSON 語法?
把文字交給解析器或線上解析工具,看它報出的第一個非法字元位置,再對照 RFC 8259 的六種值型別與空白字元規則逐一確認。頂層允許任意 JSON 值,但結構之間只允許空格、製表、換行與回車這四種空白。只要出現單引號、未加引號的鍵、尾逗號或註解,就說明它不遵守標準 JSON。
為什麼 JSON 必須用 UTF-8 且不能帶 BOM?
RFC 8259 第 8.1 節規定,用於交換的 JSON 文字必須用 UTF-8 編碼,實現不得在開頭新增 BOM,解析器可以忽略 BOM 以提高健壯性。帶 BOM 的檔案被 JSON.parse 拒絕時會報 Unexpected token,肉眼卻看不出異常。因此讀檔案時先剝掉 BOM,再顯式按 UTF-8 解碼。
是否可以用 JSON5 或 JSONC 繞過這些語法限制?
可以,但只限配置檔案這類本地讀取的場景,對外交換仍應使用標準 JSON。原文的反例表明,{"a": 1} 後面跟註解在 JSON5 和 JSONC 裡合法、在標準 JSON 裡非法,所以「配置檔案能跑、介面傳過去就報錯」幾乎總是方言問題。需要互轉時先把註解、單引號和尾逗號清掉,再交給標準解析器。