JSON RFC 8259:絆倒生產環境剖析器的細微之處
JSON 比你以為的更小
RFC 8259 裡的 JSON 文法很小:物件、陣列、字串、數字,以及三個字面值 true、false、null。這份簡單正是它無處不在的原因——也是兩個驚喜會讓人措手不及的原因。第一,任何值都能當整份文件:42、"hello"、true 各自都是完整、合法的 JSON。較舊的 RFC 4627 要求頂層必須是物件或陣列,所以有些舊工具仍會拒絕裸值。
第二,規格刻意把好幾種行為留作未指定。它精確定義了語法,卻對重複鍵、數字精度或排序著墨甚少。生產環境的剖析器以不同方式填補這些空缺,而互通性 bug 正是住在那裡。
重複鍵:未定義行為
RFC 8259 說物件名稱應該(SHOULD)唯一,而非必須(MUST)。當鍵重複時,結果未指定,所以剖析器各異:多數取最後一個值、有些保留第一個、少數拋錯、極罕見的會兩個都留。依規格而言,沒有一個是錯的。
這是一個真實的安全隱患。如果一個服務讀第一個 role 來驗證 {"role":"user","role":"admin"},而另一個服務依最後一個行事,攻擊者就能讓權限溜過驗證。在任何結果攸關的地方都把重複鍵當成錯誤,永遠別倚賴哪個值「勝出」。
數字:沒有整數,只有麻煩
JSON 只有單一數字型別。RFC 並未定義精度或範圍——它只指出,為了互通,實作被預期能處理符合 IEEE 754 雙精度的值。實務上這意味著任何超過 2^53(即 9007199254740992)的值都可能失去精度。
經典 bug:像 9007199254740993 這樣的 64 位元 ID,在 JavaScript 裡經 JSON.parse 後會變成 9007199254740992,因為數字是雙精度浮點。解法是把大整數以字串攜帶。還有幾條人們會忘的規則:不能有前導零(007 非法)、不能有前導 +、不能有結尾小數點(1. 非法),而 NaN 與 Infinity 不是合法 JSON——把它們序列化成 null 或字串。
字串、Unicode 與編碼
字串必須用雙引號,而 U+0000 到 U+001F 的控制字元必須被轉義。正斜線可選擇性地轉義成 \/,這就是為何有些編碼器會輸出 <\/script>——一種避免弄破 HTML script 標籤的合法做法。RFC 8259 規定系統間交換的 JSON 必須用 UTF-8。
基本多文種平面(BMP)以外的字元,例如大多數 emoji,要寫成一對代理(surrogate)\u 轉義,而非單一個。孤立、未配對的代理在技術上非法,但有些剖析器會接受——一個只有當你的資料跨進更嚴格的實作時才會浮現的可攜性陷阱。
看起來像 JSON 但不是的東西
最常見的錯誤來自把 JSON 當成 JavaScript。結尾逗號非法。JSON 裡不存在註解——JSON5 和 JSONC 加了它們,但嚴格的 RFC 8259 剖析器會拒絕。單引號字串和未加引號的物件鍵同樣非法,無論看起來多眼熟。
更微妙的是位元組順序標記(BOM)。RFC 8259 說產生者絕不可(MUST NOT)加 BOM、消費者可以(MAY)忽略一個,但 BOM 不是 JSON 的一部分,所以檔案開頭的 UTF-8 BOM 可能讓嚴格剖析器在第一個位元組就失敗。不具意義的空白僅限空格、tab、換行(LF)與回車(CR)——別無其他。
要點
在任何安全或正確性決策取決於值的地方拒絕重複鍵。
把 64 位元整數以字串攜帶,使它們能在像 JavaScript 那樣的雙精度剖析器中存活。
別倚賴物件鍵的順序——規格把物件定義為無序,即使多數剖析器會保留插入順序。
記住在 RFC 8259 下任何值都是合法的頂層 JSON,不只是物件和陣列。
如果你想要註解或結尾逗號,你要的是 JSON5/JSONC——在嚴格剖析器看到資料之前先轉換。本站的 JSON 格式化工具會對照嚴格的 RFC 8259 文法驗證,所以這些問題會立即浮現。