JSON 工具箱

從 RFC 8259 讀懂 JSON 語法邊界

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

入門約 11 分鐘發佈於 2026-10-30更新於 2026-10-30
從 RFC 8259 讀懂 JSON 語法邊界封面

結論:排查 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 ) ] "]"

要點只有六個:

  1. 物件用花括號,成員之間用逗號,最後一個成員後面不允許逗號;
  2. 陣列用方括號,元素之間用逗號,同樣不允許尾逗號;
  3. 數字不允許前導 +、不允許前導零(01 非法)、不允許只有小數點(1. 非法)、不允許 NaN 與 Infinity;
  4. 字串必須用雙引號,單引號非法;
  5. 布林與 null 是小寫字面量,True、NULL 都非法;
  6. 只允許空白字元 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 節說了三件事:

  1. 用於交換的 JSON 文字必須用 UTF-8 編碼;
  2. 實現不得在文字開頭新增 BOM(EF BB BF);
  3. 解析器可以忽略 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 裡非法,所以「配置檔案能跑、介面傳過去就報錯」幾乎總是方言問題。需要互轉時先把註解、單引號和尾逗號清掉,再交給標準解析器。