先選路:按你的場景挑工具
兩份 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 比對工具 為例:
- 左右各貼一份(或各上傳檔案);
- 選項按上表預設走,只在對應場景改;
- 結果預設只看差異,分欄左右對照,可切統一檢視;
- 單條差異可複製,整體結果可下載留存。
兩個工程細節:計算全程在瀏覽器裡,資料不出本機——比帶敏感欄位的回應前不用先脫敏;輸入必須是合法 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 命令列路線看整體統計。