DevTool 團隊

兩份 JSON 哪裡不一樣:從文字比對到結構比對的實踐

為什麼普通文字比對看 JSON 會滿螢幕假差異?同一個值在 JSON 裡有多種寫法——鍵序、空白、轉義形態、數字書寫形式,按行工作的 diff 工具會全部標紅。這篇講清結構化比對的思路:parse 成語法樹後按 JSONPath 報告差異,以及線上工具、jq 規範化、各語言深度比較三條路各自的適用邊界與坑(陣列順序的語意、簽名場景鍵序不能忽略、大檔案的截斷)。

先選路:按你的場景挑工具

兩份 JSON 用普通文字比對一比就滿螢幕標紅,九成不是資料變了——是 JSON 的同一個值有多種合法寫法。別再用文字 diff 比 JSON,按場景選路:

你的場景 用什麼 為什麼
偶爾比對兩份設定、介面回應 線上結構比對(如本站 JSON 比對工具) 貼進去即出帶路徑的差異清單;瀏覽器本機計算,敏感設定不用先脫敏
CI / 指令碼裡把門 jq -S 規範化 + diff -u 鍵序漂移被抹平,輸出穩定可重現,直接貼 PR
自動化測試斷言相等 語言標準庫深度比較 Node / Python / Java 全部鍵序無關,一行斷言搞定;代價是只答 true/false

三條路的共同前提是「先 parse 再比較」。下面講清楚為什麼文字 diff 會騙你、結構化報告怎麼讀、每條路具體怎麼走。

為什麼文字 diff 會騙你

同一個 JSON 值至少有四類合法寫法,每一類都足以讓按行工作的文字比對整行標紅:

假差異來源 例子 哪來的
鍵的順序 {"a":1,"b":2} vs {"b":2,"a":1} 序列化器鍵序不穩定:Go 對 map 排序、對 struct 按宣告序;Python 3.7+ dict 按插入序;不同版本 Jackson、MongoDB 匯出設定都會漂移
空白與縮排 壓縮一行 vs 縮排四格 介面回傳無空白,人看過的檔案被編輯器格式化過——一格式化每行都「變」
轉義形態 "你好" vs "\u4f60\u597d" 有的庫預設非 ASCII 全轉義,有的直接輸出原字元
數字書寫形式 1000 / 1e3 / 1000.0 解析後同一個數,寫法不同

反方向的坑同樣存在:把壓縮 JSON 格式化後再 diff,行數全變,「差異行數」徹底失去參考意義。

結構化比對報告怎麼讀

結構化比對先把兩份文字 parse 成語法樹,再遞迴比較節點,報告的是帶路徑的條目:

replace  $.version         "1.4.0" → "1.5.0"
replace  $.retries         3 → 5
add      $.features[2]     "giftcard"
remove   $.limits.maxItems 50

每條三件事:操作(新增/刪除/替換)、路徑(JSONPath 定位到欄位)、兩側的值。四類寫法差異在 parse 後天然消失。這種報告可以直接對著 API 文件或設定 schema 讀——「version 升到 1.5.0、retries 從 3 改 5」——而不是「第 7 行和第 9 行不一樣」。

三個比對選項對應三個真實決策,預設值都是對的,只在明確需要時改:

選項 預設 什麼時候改
忽略鍵序 開 除錯 webhook 簽名這類按位元組計算的協定時關掉,讓鍵序差異顯形
忽略陣列順序 關 只在確認陣列是集合語意(權限清單、標籤)時開——開了 [1,2] 與 [2,1] 判等,用錯場景會掩蓋真實回歸
忽略大小寫 關 比較大小寫不敏感的設定體系時再開

三條路各自怎麼走

線上工具(一次性比對最省事)

以本站 JSON 比對工具 為例:

  1. 左右各貼一份(或各上傳檔案);
  2. 選項按上表預設走,只在對應場景改;
  3. 結果預設只看差異,分欄左右對照,可切統一檢視;
  4. 單條差異可複製,整體結果可下載留存。

兩個工程細節:計算全程在瀏覽器裡,資料不出本機——比帶敏感欄位的回應前不用先脫敏;輸入必須是合法 JSON,解析失敗會標出哪一側在什麼位置出錯,不會把解析錯誤偽裝成海量差異。

jq(CI 與指令碼)

jq -S . old.json > /tmp/old.canonical.json
jq -S . new.json > /tmp/new.canonical.json
diff -u /tmp/old.canonical.json /tmp/new.canonical.json

-S 排序所有物件鍵,空白也由 jq 統一——diff 輸出穩定可重現,貼 PR 描述正合適。它抹不掉陣列順序與大小寫差異,也不報告「哪個路徑上什麼操作」:jq 回答「有沒有實質變化」,不回答「變了什麼」。這個邊界正好和線上工具互補。

程式碼斷言(自動化測試)

  • Node.js:util.isDeepStrictEqual(a, b);
  • Python:json.load 之後 a == b(dict 相等天然鍵序無關);
  • Java:objectMapper.readValue(json, Map.class) 再 assertEquals。

只答 true/false,失敗時不告訴你差在哪個路徑——所以常見組合拳是 CI 裡標準庫斷言守門,失敗時人來線上工具看差異定位。

邊界與真話

瀏覽器結構比對有容量上限。 本站單側 2MB、差異條目兩萬條封頂——不是隨手定的:規範化會讓壓縮 JSON 膨脹 3~5 倍,記憶體與渲染按展開後算;渲染不虛擬化時幾萬行 diff 會拖垮頁面。超限會明確提示「已截斷」而非假裝比完。幾十 MB 的匯出與全量轉存,老實走 jq。

線上工具不是所有場景的最優解。 需要可重現的證據鏈(PR、CI 門禁、code review)時,jq 規範化輸出的文字 diff 比截圖或「我線上看過了」可信得多;測試斷言場景裡,標準庫一行程式碼比開網頁快。線上工具的領地是人要讀的差異報告——路徑、兩側值、分欄對照。

陣列順序的判斷責任在你。 工具不知道 ["read","write"] 是權限集合還是有序流水。順序參與語意(時間軸、優先權佇列)就別開忽略;確認集合再開。

常見問題

為什麼文字比對工具看 JSON 會報一堆假差異?

因為 JSON 的同一個值有多種合法寫法:鍵的順序、空白縮排、字串轉義形態(\u4f60 與「你」)、數字書寫形式(1e3 與 1000)。文字比對按行工作,任何一類寫法差異都讓整行標紅,哪怕行內的值一個都沒變。結構化比對先 parse 成語法樹再比較節點,這四類形態差異在解析後就消失了,報告出來的才是值的真實變化。

jq 怎麼在命令列裡比對兩份 JSON?

先用 jq -S 把兩份檔案規範化(所有物件鍵按字母序輸出),再用 diff -u 比較規範化結果:jq -S . old.json 與 jq -S . new.json 各存一份暫存檔後 diff。鍵序漂移被抹平,輸出適合貼進 PR 描述。局限是它本質仍是文字比對——陣列順序、大小寫差異抹不掉,也只回答「有沒有變」,不擅長報告「具體哪個路徑變了」。

鍵的順序什麼時候不能忽略?

絕大多數場景鍵序沒有語意,但存在例外:對 JSON 序列化後的位元組串做雜湊或簽名的協定(webhook 簽名驗證、部分 API 的請求簽名)按實際位元組計算——鍵序不同、值相同的兩份 JSON,簽名輸入並不相同。除錯這類問題時應該關閉「忽略鍵序」選項,讓鍵序差異顯形;日常比對設定與 API 回應時則保持開啟,消除序列化器鍵序漂移的噪音。

比對結果被截斷是什麼意思?

結構比對工具給差異條目設了渲染上限(本站為兩萬條),超過後明確提示已截斷,而不是靜默丟棄。被截斷是一個訊號:差異面已經大到逐條閱讀沒有意義——更有效的做法是先用格式化工具把兩份輸入規範化縮小差異範圍,或者改走 jq 命令列路線看整體統計。