避免 JSON Diff 錯誤最可靠的方式,是把這個工具視為結構變更檢查員,而非文字編輯器、修補引擎或移動偵測器。JSON Diff 會將兩個嚴格的 JSON 值視為已解析的資料來進行比較,而非以格式化後的位元組呈現,並在 RFC 6901 JSON Pointer 路徑上回報新增、移除、變更與型別變更的項目,因此讀者犯下的幾乎所有錯誤,都來自於套用了從其他類別工具借來的操作期待。這個工具刻意忽略空白與物件成員順序,因為真正的 JSON 物件根據 RFC 8259 是無序的集合,而它也刻意以位置方式回報陣列,不會自行猜測項目的身分。這項單一設計決策,就排除了使用者在初次接觸時會遇到的一半誤判投訴:在其中一邊變更鍵的順序並不會產生幽靈差異,而將項目從索引 2 移到索引 5 則會產生多個差異。再加上嚴格的解析規則(不接受註解、不接受尾端逗號、不接受 undefined 或 NaN)、RFC 6901 的 JSON Pointer 跳脫規則,以及每個數值輸入都採用 JavaScript Number 精度,犯錯的面向就寬廣到值得在貼上大量承載資料之前先列一份檢查清單。

how do i avoid mistakes when i use json diff
避免常見的 JSON Diff 錯誤:實用指南

為什麼 JSON Diff 錯誤來自類別混淆

大多數 JSON Diff 錯誤來自於套用了從其他類別工具借來的操作期待。如果你多年來一直使用以行為基礎的文字差異工具,那麼看到兩個 JSON 文件時,你的第一反應會是逐字元比較,注意縮排、大括號位置與尾端逗號。這個直覺立刻就會產生誤判,因為 RFC 8259 使空白在符記之間不具意義,並明確將物件定義為無序的成員集合。

相較之下,JSON Diff 在進行任何比較之前,會先用嚴格的 RFC 8259 解析器解析兩邊的內容,因此在計算第一個新增或移除項目之前,縮排、換行與物件成員順序都已被正規化消除。如果讀者在兩邊貼上同一份但鍵順序不同的文件,將會看到一個空的變更清單,而正確的反應是感到釋然,而不是驚訝。這個工具不會把格式上的漂移標記為語意差異,這代表任何確實出現的變更,都是值得調查的真實數值、鍵、型別或陣列位置變更。

另外三種類別的混淆也同樣經常出現。讀者預期會看到 JSON Patch 輸出(RFC 6902 操作),但實際收到的卻是面向審查的變更清單。讀者預期陣列重新排序會被彙總為單一移動,但實際的比較卻嚴格按照位置進行。讀者預期會有任意精度整數運算,但解析器使用的是 JavaScript 的 IEEE 754 Number 型別,並在比較前先對部分輸入進行四捨五入。了解你實際身處哪一個類別,是本指南後續每個章節的先決條件。

把變更清單當成可執行修補

讀者在使用 JSON Diff 時最昂貴的一項錯誤,就是把複製出來的輸出當成 RFC 6902 JSON Patch,並套用至來源文件。這個輸出既不是 JSON Patch,也不是 JSON Merge Patch,也不是統一的文字差異,更不是結構描述遷移。它是一份產品定義的變更清單,記錄每個新增、移除、變更與型別變更的項目,以及其 RFC 6901 JSON Pointer 路徑和變更前後的值。

結構上的差異很重要。JSON Patch 包含 add、remove、replace、move、copy 與 test 等具有套用語義的操作,而 JSON Diff 的變更清單完全不含操作,僅有觀察結果。這個工具從未聲明某個被移除的值可以在周圍變更後還原,從未發出 move 或 copy,也從未測試任何前置條件。如果你把變更清單貼進 JSON Patch 函式庫,期待它能重播,該函式庫將拒絕每一個項目。

安全的工作流程是將變更清單用於審查與 pull request,然後在任何變更自動套用之前,使用一個有維護的修補實作與針對應用程式專屬的測試。請自行審查各個項目,將每個路徑對應到你的應用程式實際需要的操作,並在信任結果之前,先驗證產生的文件能夠通過你的解析器完成來回轉換。

誤讀 RFC 6901 路徑跳脫

JSON Pointer 路徑看起來像檔案路徑,但其跳脫規則會讓沒讀過 RFC 6901 的讀者上當。在參考符記中,有兩個字元具有特殊意義:斜線 / 用來分隔符記,而波浪號 ~ 則代表跳脫序列的開頭。包含這兩個字元之一的成員名稱必須經過編碼,否則像 /first/name 這樣的路徑會默默指向名為 first 與 name 的兩個子項目,而不是名為 first/name 的成員。

跳脫規則是精確且不對稱的。成員名稱中的字面斜線會變成 ~1,而字面波浪號則會變成 ~0。先後順序很重要:~1 必須在 ~0 之後套用,以避免雙重編碼。陣列位置使用純粹的十進位符記,例如 /0、/1、/12,而文件的根則是空字串,在人類可讀的清單中顯示為 root,而非前置的斜線。

如果讀者複製像 /settings/ui~1theme 這樣的路徑,並假設它指向名為 ui/theme 的子項目,就會讀錯成員。正確的路徑是 /settings/ui~1theme —— 符記中間的 ~1 是真實的跳脫序列,而不是分隔符號。當你把這些路徑貼回自己的程式碼時,請依序將 ~1 解碼為 /、將 ~0 解碼為 ~,再把結果視為成員名稱。

成員名稱中的字元編碼後的符記
/ (斜線)~1
~ (波浪號)~0
純英數或底線保持不變

把陣列重新排序誤判為移動

JSON Diff 以位置方式比較陣列,僅回報實際情況,不多也不少。陣列在 JSON 中是有序的序列,比較時會依序比對索引 0 對索引 0、索引 1 對索引 1,依此類推。這個工具不會猜測身分鍵、不會計算最長共同子序列,也不會把陣列當作集合處理,因為這些策略取決於資料契約,而且可能會隱藏具有意義的順序變更。

實際上的結果是,將項目從索引 2 移到索引 5 會以一連串變更呈現,而不是一個乾淨俐落的移動。位於舊索引的項目會被回報為移除,新索引處會出現帶有正確值的新項目,而中間的所有索引則會因為其內容在位置上不再相符而被標記為變更。期望以單一差異行來呈現重新排序的審查者,將會誤判這項變更的規模。

如果你的應用程式將陣列順序視為語意上無意義,請在貼上之前將兩邊正規化為一致的順序。如果你的應用程式將陣列順序視為有意義,請照字面解讀這個位置連鎖,並檢視新順序是否就是實際想要的結果。

數字精度與數值邊界情況

所有數值輸入在進行任何比較之前,都會經過 JavaScript 的 IEEE 754 Number 型別處理,而這項選擇帶來讀者應該預期的三個後果。

首先,當解析器將整數交給比較階段時,超過安全整數上限的整數可能已經被四捨五入。安全整數上限的定義為 2^n − 1,其中 n 為尾數位元數,而 JavaScript 定義 n = 53。代入後,上限等於 2^53 − 1 = 9007199254740991。任何絕對值超過 9007199254740991 的整數,都不保證能夠在比較中完成來回轉換,因此兩個僅在第十六個有效位數之後才有所不同的大整數字串,可能會產生不正確的「無變更」結果。

其次,細微的小數差異無法保留。像 0.1 + 0.2 = 0.30000000000000004 這樣的值,在解析後的表示中已然存在,因此數學上相等但文字上不同的變更後值,可能會、也可能不會相符,這取決於解析器實際產生了哪些位元組。如果你需要精確的小數行為,請使用專為該契約設計的解析器與數值模型來比較來源。

再次,正零與負零會被視為相同的 JSON 數值進行比較,因為 JavaScript 中 -0 === 0 為真。期望符號差異會被回報的讀者,將會對空的變更清單感到驚訝。如果是金融或帶正負號大小運算的工作,請直接比較來源的表示法,而不是依賴瀏覽器的算術運算。

如何在沒有常見陷阱的情況下進行 JSON Diff

這是能避免上述所有錯誤的工作流程。

  1. 在貼上之前,先用嚴格的 RFC 8259 解析器驗證兩邊的內容。註解、尾端逗號、undefined、NaN、Infinity、BigInt 語法以及 JavaScript 物件實字,都會被 JSON Diff 拒絕,而無法解析的一邊根本不會產生任何比較結果。
  2. 正規化你的期待,而非你的資料。決定在你的契約中,物件鍵的順序是否重要(幾乎從來不重要)、陣列順序是否重要(通常重要),以及 JavaScript Number 的精度是否能接受你所處理的數值。
  3. 將左右兩份文件貼入 JSON Diff —— 格式與物件成員順序可以不同,工具會將它們視為等價。
  4. 執行比較,並依照其 RFC 6901 JSON Pointer 路徑檢查每個新增、移除、變更或型別變更的項目。確認路徑解碼正確,特別留意 ~1 代表 /、~0 代表 ~。
  5. 對於陣列重新排序,請計算項目數量。remove + add + 多個變更的連鎖代表位置回報,而不是錯誤,並代表這是真實的重新排序,而非邏輯上的移動。
  6. 複製變更清單以供審查。請勿將其貼入期待 RFC 6902 語義的 JSON Patch 函式庫;輸出僅是帶有變更前後值的、面向審查的變更清單。
  7. 在任何變更自動套用之前,為你的資料契約打造專屬的修補實作與測試。將這些測試套用於生產文件的快照,並驗證來回轉換。
  8. 在將敏感的變更前後值複製到記錄、工單或聊天串之前,請先加以檢視。工具會在本機執行,但審查軌跡未必如此。

Inputs the Tool Refuses: What to Do Instead

JSON Diff is strict on purpose, and several common JSON-ish inputs are rejected rather than silently coerced. Comments of any style, trailing commas, the JavaScript literal undefined, the IEEE special values NaN and Infinity, BigInt syntax with the n suffix, and bare JavaScript object literals that omit quotes around keys all fail strict RFC 8259 parsing. The right response is to remove the offending syntax with a real JSON parser, then paste the cleaned result.

Duplicate member names in the source text are also unaddressable by the tool, because normal JSON parsing collapses duplicates into a single entry before the comparison sees the object. If your data has duplicate keys and the duplicates are meaningful, you need a non-standard parser that preserves them, then a hand-written diff on the resulting structure.

Each input is bounded at 500,000 characters, nesting to 50 comparison levels, and the output is bounded at 5,000 changes. The tool fails rather than truncating a larger result, which protects you from a silent partial answer that looks complete. If your documents exceed those limits, split the comparison by a known partition key or use a streaming differ designed for the volume.

Quick Reference: Change Types

Change typeTriggerWhat the entry contains
addedKey appears only on the right, or array position exists only on the rightRFC 6901 path and the right value
removedKey appears only on the left, or array position exists only on the leftRFC 6901 path and the left value
changedSame path, same JSON type, different valueRFC 6901 path, left value, and right value
type-changedSame path, different JSON type (for example number to string, object to array, or null to boolean)RFC 6901 path, left value, and right value

Each entry is reported against an RFC 6901 JSON Pointer, and object member names are sorted for deterministic reporting, so two runs over the same pair of documents will produce the same change list in the same order. Subtrees can appear as values when a whole member is added, removed, or changes type, which keeps the output readable even when the structural difference is large. The eight external fixtures the tool is verified against — changed, added, removed, type-changed, array index, slash escaping, tilde escaping, and object-order equality — give a fixed regression surface you can also test against your own expectations before trusting a result in production.