JSON 對比工具怎麼選:文字 diff、結構 diff 與視覺化
結論:文字 diff 負責找出「哪裡不一樣」,結構 diff 負責判斷「內容是否等價」,視覺化負責讓人快速看懂差異。三層選對,才不會把鍵順序當成事故。

結論:文字 diff 用於發現差異(先 jq -S 排序鍵再比),結構 diff 用於判斷等價(陣列按主鍵對齊),視覺化只用於解釋與複核;CI 裡三層分工,不能混成一層。
兩份 JSON 長得不一樣,不代表它們的內容不一樣;先選對比較的層次,再選工具。
一、三種「對比」其實不是一回事
很多人把「對比 JSON」當成一個動作,結果在不同場景裡反覆踩坑。至少要拆成三層:
| 層次 | 比較對象 | 能發現什麼 | 典型場景 |
|---|---|---|---|
| 文字 diff | 原始位元組與行 | 縮排、鍵順序、空格、換行 | 配置漂移、程式碼評審 |
| 結構 diff | 解析後的物件與陣列 | 鍵增刪、值變化、陣列元素變化 | 介面回歸、資料遷移 |
| 視覺化 diff | 差異的呈現方式 | 差異落在哪一層、影響範圍多大 | 人工複核、團隊溝通 |
文字 diff 的優點是零依賴、速度快、和 Git 天然整合;缺點是它不知道 JSON 的語義。兩份完全等價的文件,只要鍵順序或縮排不同,就會顯示成大片紅綠。結構 diff 則相反:它先解析再比較,能識別「同一個值換了位置」,但需要選對陣列對齊策略,否則會把一次插入誤判成整段重寫。
視覺化不是第三種演算法,而是把前兩者的結果畫出來。它適合讓人快速確認「差異是不是預期的」,不適合替代自動化判斷。把三層混在一起,最常見的結局是:CI 天天紅,大家逐漸學會忽略它。
二、文字 diff:先用 jq -S 清掉鍵序雜訊
如果只想快速看兩份文件的差異,最省事的組合是 jq -S 加 diff -u。-S 會遞迴排序物件鍵,讓「內容相同但鍵序不同」的文件先歸一化:
jq -S . a.json > a.sorted.json
jq -S . b.json > b.sorted.json
diff -u a.sorted.json b.sorted.json
這一步解決的是最常見的假差異。RFC 8259 明確規定物件是無序集合,{"a":1,"b":2} 與 {"b":2,"a":1} 是同一個值;但文字 diff 會把它們全部標成變更。先排序再比較,紅色區塊才更接近真實問題。
注意 jq -S 只排序鍵,不排序陣列。陣列順序在 JSON 裡是有意義的,[1,2] 和 [2,1] 不是同一個值,所以工具不會替你做這個決定。若業務上陣列本來就是無序集合,需要自己按主鍵排序後再比較。
還有一個容易忽略的細節:diff 在發現差異時退出碼是 1,而被比較文件不存在、權限不足等錯誤也會返回非 0。在 CI 裡不能簡單地把「非 0」當成「有差異」,否則一次路徑寫錯會被誤報成內容變更。更穩的寫法是先確認文件存在,再單獨處理差異結果:
if ! diff -u a.sorted.json b.sorted.json; then
echo "JSON 內容存在差異" >&2
exit 1
fi
delta 讓文字 diff 真正可讀
原生 diff -u 的輸出能用,但長行、深嵌套的 JSON 讀起來很累。delta 是一個 diff 過濾器,可以給差異加上語法高亮、行號與更清晰的背景色:
diff -u a.sorted.json b.sorted.json | delta
它原本是 Git 的分頁器,所以也能直接接管 git diff。對於「每天要看十次配置差異」的人,這類工具提升的是閱讀速度,而不是演算法精度。另一個常見選擇是 git diff --no-index --word-diff,把比較粒度降到詞一級,適合單行很長的壓縮 JSON。
不過要提醒一句:美化不會消除假差異。如果鍵順序沒歸一化,delta 只會把雜訊顯示得更漂亮。正確順序永遠是先規範化,再美化。
三、結構 diff:jsondiffpatch 才懂語義
需要判斷「兩份資料在語義上是否等價」時,應該用結構 diff。JavaScript 生態裡最常用的是 jsondiffpatch,它輸出的是一份描述變更的 delta 物件,而不是帶 +、- 的文字行:
import { create } from 'jsondiffpatch';
const instance = create();
const delta = instance.diff(
{ id: 1, tags: ['a', 'b'], enabled: true },
{ id: 1, tags: ['a', 'c'], enabled: false },
);
console.log(JSON.stringify(delta, null, 2));
delta 的結構會區分「新增鍵」「刪除鍵」「值被替換」「陣列元素被移動」。這意味著你可以直接走訪 delta 做斷言:沒有差異時 delta 為 undefined,有差異時按路徑逐條檢查。和文字 diff 相比,它不會被縮排、鍵序、數字寫法(1e3 與 1000)干擾。
同樣重要的是,結構 diff 的結果可以直接接到補丁流程。jsondiffpatch 提供 patch 與 reverse,能把 delta 套用到原物件並撤銷。但要注意:自動生成的補丁和你手工寫的 JSON Patch 不是同一套格式,跨工具使用前需要轉換。差異比較的完整思路可以對照 JSON 對比與差異。
四、鍵順序造成的假差異怎麼徹底消除
鍵順序問題不只是「看起來煩」,它會直接製造錯誤結論。排查時按下面三條走:
- 比較前統一用
jq -S或等價工具遞迴排序鍵; - 刪除每次都會變化的欄位,例如
timestamp、requestId、traceId; - 確認沒有重複鍵。重複鍵在解析階段就已被「後者覆蓋」,文字 diff 能看到,結構 diff 看不到。
jq -S 'del(.meta.timestamp, .meta.requestId)' a.json > /tmp/a.json
jq -S 'del(.meta.timestamp, .meta.requestId)' b.json > /tmp/b.json
diff -u /tmp/a.json /tmp/b.json
這套流程適合配置漂移排查。把 staging 與 production 的配置都過一遍,剩下的差異才是真正需要解釋的。動態欄位的過濾規則最好寫進指令碼並納入版本控制,而不是每次靠記憶手敲。編輯與指令碼側的配套工具可以看 編輯器、命令列與自動化工具鏈。
五、陣列到底按索引還是按 id
陣列是 JSON 對比裡最容易出錯的部分。預設按索引對齊時,在開頭插入一條記錄,後面所有元素都會「錯位」,工具會把它們逐個標成變更。這不是工具壞了,而是對齊策略選錯了。
| 對齊方式 | 適用前提 | 優點 | 風險 |
|---|---|---|---|
| 按索引 | 陣列順序有業務含義 | 簡單、無需額外欄位 | 一次插入造成連鎖誤報 |
| 按主鍵 | 元素是帶唯一 id 的集合 | 能識別插入、刪除、移動 | 需要穩定且唯一的主鍵 |
| 最長公共子序列 | 順序重要但允許中間插入 | 比純索引更穩 | 結果不易解釋、效能開銷更大 |
jsondiffpatch 用 objectHash 指定主鍵,陣列元素會先建索引再比較:
const instance = create({
objectHash: (item: { id?: string; sku?: string }) => item.id ?? item.sku,
});
如果只用命令列的 jq,可以先排序再走文字 diff:
jq -S 'sort_by(.id)' items.json > items.sorted.json
按 id 對齊的代價是「位置變化」不再算差異。如果業務需要知道某條記錄從第 2 位挪到了第 5 位,就必須額外比較順序欄位,不能只看集合內容。
六、視覺化對比:給人看的那一層
結構 diff 的輸出適合機器,不適合開會。需要向別人解釋差異時,視覺化工具更有效:
- VS Code 的 diff 編輯器適合逐檔案核對,左右並排、行內高亮;
- JSON Crack、JSON Hero 這類樹形檢視器適合看差異落在哪一層;
jsondiffpatch自帶 HTML formatter,可以把 delta 渲染成可摺疊的富文字檢視。
視覺化工具的價值在「定位上下文」:文字 diff 告訴你第 88 行變了,樹形檢視告訴你第 88 行屬於 data.items[3].price。但視覺化通常需要把完整文件載入瀏覽器或桌面端,處理生產報文前要先確認它是本地計算還是上傳到伺服器。
七、把對比放進 CI 的三條實踐
第一,先規範化再比較。固定縮排、遞迴排序鍵、過濾動態欄位,三步做完再進入 diff。否則 CI 會因為無關雜訊反覆失敗。
第二,按語義選對齊方式。陣列是集合就用主鍵,陣列是序列就保留索引,不要用一套規則套所有介面。
第三,斷言值而不是斷言文字。測試裡比較解析後的物件,比比較 JSON.stringify 的結果穩定得多。需要輸出失敗資訊時,只列印差異路徑和前後值,不要把整份報文倒進日誌。
# 只輸出差異路徑與前後值,避免日誌被大檔案淹沒
jq -S 'del(.timestamp)' actual.json > /tmp/actual.json
jq -S 'del(.timestamp)' expected.json > /tmp/expected.json
diff -u /tmp/expected.json /tmp/actual.json | head -n 80
如果差異需要長期追蹤,可以把規範化指令碼和期望檔案一起入庫;如果只是一次性排查,臨時指令碼用完就刪,別讓它悄悄變成第二套業務邏輯。
八、工具選擇表
| 場景 | 首選 | 備選 | 關鍵提醒 |
|---|---|---|---|
| 偶爾看兩份檔案 | VS Code diff | diff -u |
先格式化,減少雜訊 |
| 配置漂移排查 | jq -S + diff |
delta |
過濾動態欄位 |
| 介面回應回歸 | jsondiffpatch |
deepdiff |
陣列按主鍵對齊 |
| 給團隊講解差異 | JSON Crack / JSON Hero | HTML formatter | 注意資料是否上傳 |
| 超大檔案 | jq 抽取子樹 |
jless 瀏覽 |
不要整棵載入記憶體 |
| Git 提交前檢查 | git diff --no-index |
pre-commit | 退出碼要單獨處理 |
沒有「最好的 JSON 對比工具」,只有「當前這層該用什麼工具」。文字 diff 負責發現變化,結構 diff 負責判斷等價,視覺化負責解釋變化。三層各司其職,比找一個全能工具可靠得多。跨分支合併時的衝突處理,可以繼續看 JSON 與 Git 合併。
常見問題
jq -S 會不會把陣列順序也改掉?
不會。-S 只對物件的鍵排序,陣列保持原有順序。JSON 裡陣列是有序的,工具不會替你把 [1,2] 變成 [2,1];如果業務上陣列是無序集合,需要自己用 sort_by 明確排序。
1 和 1.0 在結構 diff 裡算同一個值嗎?
大多數解析器會把它們都解析成同一個數字,因此結構 diff 通常認為相等;文字 diff 則會標成差異。判斷「是否等價」要用結構 diff,判斷「檔案是否逐位元組一致」才用文字 diff。
對比結果能直接當 JSON Patch 用嗎?
不能直接替換。對比工具輸出的是「哪裡不一樣」,JSON Patch 要求的是「按順序執行哪些操作」,兩者格式和語義都不同。自動生成的補丁還常用陣列索引定位,並發場景下索引會失效,套用前建議加 test 校驗。
幾十 MB 的 JSON 怎麼對比?
先抽取關心的子樹,例如 jq '.data.items',再按主鍵排序後比較;不要一上來就把整棵樹載入記憶體做遞迴 diff。超大檔案更適合串流處理或分片比較,記憶體與耗時的數量級需要單獨評估。
線上對比工具會不會洩露資料?
取決於實作。純前端工具在瀏覽器裡計算,資料不上傳;伺服器端渲染的工具會把內容送到後端,可能進入日誌。對比生產配置或使用者報文前,先確認頁面是否本地計算,必要時直接斷網測試一次。
如何決定先用文字 diff 還是結構 diff?
只想快速看兩份檔案哪裡不同,先用 jq -S 排序鍵再跑 diff -u;要判斷兩份資料在語義上是否等價,用 jsondiffpatch 這類結構 diff,並按主鍵對齊陣列。文字 diff 零依賴、和 Git 天然整合,結構 diff 不會被縮排、鍵序和數字寫法干擾。
為什麼 CI 裡不能把 diff 的非零退出碼都當成有差異?
diff 發現差異時退出碼是 1,但檔案不存在、權限不足等錯誤也返回非 0。CI 裡簡單地把非 0 當成「有差異」,會把一次路徑寫錯誤報成內容變更。更穩的寫法是先確認檔案存在,再單獨處理差異結果。
是否需要在瀏覽器裡對比幾十 MB 的檔案?
不建議。幾十 MB 的檔案應先用 jq 抽取關心的子樹,再按主鍵排序後比較;整棵樹載入記憶體做遞迴 diff,記憶體與耗時都不划算。超大檔案更適合串流處理或分片比較。