比較兩個 JSON 檔案時,會將兩份文件都解析為資料、以遞迴方式遍歷,並在 RFC 6901 JSON Pointer 路徑上回報一組具確定性的新增、移除、變更與型別變更項目。對兩個 JSON 檔案進行純文字差異比較會產生雜訊:重新排版、鍵值排序或零星空白都會顯示為變更,但實際上並未改變資料。JSON Diff 工具所採用的結構化方法,會將兩側都解析為嚴格的 RFC 8259 JSON,依物件自身的成員名稱進行比較,而不依賴來源順序,並以零為起始的位置索引來比較陣列。輸出結果是一份審查清單,每一處差異都標記在精確的 JSON Pointer 路徑上(也就是驅動 JSON Patch 與其他標準的同一種路徑語法),並附上左側值、右側值或兩者。這讓你對兩份文件的歧義之處擁有單一真相來源,無論該歧義是值的變更、型別的變更,或是全新或缺漏的分支。

compare two json files
比較兩個 JSON 檔案:找出每一處 JSON Pointer 的變更

結構化 JSON 比較真正的意涵

兩個 JSON 檔案之間的文字差異會顯示每一個變更的位元組,但這很少是你真正想要的。兩個語意上相同的 JSON 值,在磁碟上可能會有幾十處差異:美化排版與緊湊排版、鍵的順序不同、Tab 與空格混用、尾端換行、數字寫成 1 而非 1.0。對資料的消費者而言,這些都不是有意義的差異,但每個位元組層級的差異工具卻都會把每一處標記為變更。

結構化比較會先把兩份文件解析為 JSON,再走訪產生的物件圖。空白與成員順序不再屬於輸入的一部分。比較本身會變成一次遞迴走訪,將每一個觀察到的差異歸類為四種之一:新增、移除、變更或型別變更。每個項目都帶有發現差異的 RFC 6901 JSON Pointer 路,以及相關的左側值或右側值(或兩者)。JSON Pointer 是在 RFC 6901 中定義的路徑語法,建立在符合 RFC 8259 的 JSON 之上,因此變更清單的讀法與 JSON Patch 文件相同。若想在編輯器中使用純文字並排比較的工作流程,Diff Checker 是合適的搭檔——請參考 VS Code 文字差異覽器工作流程指南 來了解對比差異。若是資料層級的審查,結構化比較才是正確的工具。

如何在瀏器中比較兩個 JSON 檔案

  1. 為兩側準備嚴格符合 RFC 8259 的 JSON。若任何一側含有尾端逗號、註解或 undefined 值,請先用 JSON Formatter 之類的格式化工具或 JSON Validator 等驗證工具先行修正,因為嚴格解析會直接拒絕這類輸入。
  2. 開啟 JSON Diff 工具,將左側值貼入左邊輸入欄,右側值貼入右邊輸入欄。兩側的排版與物件成員順序可能不同,這並不會產生雜訊。
  3. 執行比較並檢視產生的清單。每一列會識別出一處差異、觀察到該差異的 JSON Pointer 路徑、先前的值(左側),以及適用的新值(右側)。
  4. 將變更清單作為審查附件複製到程式碼審查、工單或聊天討論串中。請將複製的文字視為文件說明,而非可執行的操作。
  5. 若打算自動套用這些變更,請以經維護的 patch 函式庫與具確定性的 JSON 解析器撰寫測試;變更清單本身並非可執行的 patch,其路徑描述的是觀察到的位置,並非保證可執行的操作。

在 JSON Pointer 路徑上,每個差異如何被分類

結果中的每個項目都屬於四種之一,並在各種情況下回報相同的欄位。文件的根是空白的 JSON Pointer;當兩個值在最頂端就不同時,工具會在可讀的清單中顯示為 root

類型路徑包含左側值右側值
Added(新增)該值出現的 JSON Pointer—(無)右側的新值
Removed(移除)該值原本所在的 JSON Pointer左側的舊值—(無)
Changed(變更)該位置的 JSON Pointer同 JSON 型別的先前值同 JSON 型別的新值
Type-changed(型別變更)該位置的 JSON Pointer帶原始型別的舊值帶新型別的新值

當整個成員被新增、移除或型別變更時,物件與陣列的子樹可以作為一個值出現,因此報告不限於基本型別。JSON Pointer 的跳脫符合 RFC 6901:成員名稱中的字面斜線會變成 ~1,字面波浪號會變成 ~0,而陣列位置則使用十進位索引代碼。若將路徑複製到其他工具,請保持該跳脫完整,否則路將在不知情的情況下指向錯誤的位置。

為何忽略物件順序,但不忽略陣列順序

JSON 物件是無序的名稱/值配對集合,比較也反映了這一點。它會走訪兩側自身的可列舉成員,為了具確定性的回報而對鍵進行排序,並依成員名稱比較值。僅出現在右側的鍵會成為 added 項目;僅出現在左側的鍵會成為 removed 項目;兩側都有的鍵則以遞迴方式比較。重新排序原始文字中的鍵完全不算是差異,因為解析後的物件對應關係是相同的。原始 JSON 文字中的重複名稱無法作為獨立項目進行比較,因為嚴格的 JSON 解析器在比較看到值之前,就已將其合併為單一對應。

JSON 陣列是有序的序列,依零起始位置進行比較。若某個值從索引 2 移到索引 5,結果會在索引 2 出現一筆移除、在索引 5 出現一筆新增,以及在被取代元素所落腳之處出現多筆 changedtype-changed 項目,而不是將其視為單一的語意移動。該工具不會猜測身份鍵、不會計算最長共同子序列,也不會將陣列視為集合;這些策略取決於資料合約,且可能掩蓋消費者所關心的有意義的順序變更。

限制、拒絕情況與何時該改用其他工具

嚴格的 RFC 8259 解析意味著註解、尾端逗號、undefinedNaNInfinity、BigInt 語法以及 JavaScript 物件實字都會被拒絕。若輸入來自寬鬆的方言,請先以支援 JSON5 的標準化工具處理,否則比較根本不會開始。

每一側可以是任何 JSON 值:物件、陣列、字串、數字、布林值或 null。比較若要順利完成,必須遵守的限制如下:

  • 每個輸入最多 500,000 個字元;
  • 最多 50 層巢狀的比較深度;
  • 結果中最多 5,000 個差異。

若結果會超過 5,000 個差異,工具會直接失敗,而非截斷。這是刻意的設計,因為被截斷的變更清單可能讓自動化的 patch 套用指向與你預期不同的文件。當接近這些上限時,請規劃縮小輸入範圍、修正資料形狀,或分區域進行比較。

數字會被解析為 JavaScript Number,因此在比較執行之前,位於安全範圍之外的整數與細微的小數差異可能已經被四捨五入。正零與負零會被視為相同的 JSON 數值。若你的合約在意精確的小數表示、重複的物件名稱,或是任意精度的整數,請勿依賴此瀏覽器內部的表示方式。請使用為該合約設計的解析器與數值模型,或在決定兩個解析後的數字是否真的相等之前,先透過 IEEE 754 轉換器 檢視其位元層級的 IEEE 754 編碼。

在使用變更清單之前,請先閱讀它

JSON Diff 的輸出是產品定義的變更清單,具確定性排序。它刻意不採用 RFC 6902 JSON Patch、JSON Merge Patch、文字統一差異或結構描述遷移。當中不包含 movecopytest 等操作,且被移除的值在周圍變更已被回報之後,無法假設其可被還原。請將此清單視為結構化的審查附件:在決定如何處理之前,將其複製到 pull request 說明、附加到 bug,或與團隊成員分享。

當自動化套用合適時,請採用一套針對你的資料儲存或文件的經維護 patch 實作,並搭配應用程式專屬的測試。共有八個外部測試案例涵蓋標準情況:變更、新增、移除、型別變更、陣列索引、斜線跳脫、波浪號跳脫,以及物件順序相等。額外的測試涵蓋具確定性的巢狀變更與帶正負號的零,因此比較本身的行為是穩定的,但「比較回報了位置」與「套用此變更會產生我想要的文件」之間的落差確實存在。在將變更清單複製到會持久保存的地方(例如聊天頻道或工單內文)之前,請務必檢視敏感的變更前後值。

延伸閱讀:在 IntelliJ IDEA 中格式化 JSON:快捷鍵與瀏覽器