JSON 工具箱

JSON 對比工具怎麼選:文字 diff、結構 diff 與視覺化

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

進階約 9 分鐘發佈於 2026-10-31更新於 2026-10-31
JSON 對比工具怎麼選封面

結論:文字 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 對比與差異。

四、鍵順序造成的假差異怎麼徹底消除

鍵順序問題不只是「看起來煩」,它會直接製造錯誤結論。排查時按下面三條走:

  1. 比較前統一用 jq -S 或等價工具遞迴排序鍵;
  2. 刪除每次都會變化的欄位,例如 timestamp、requestId、traceId;
  3. 確認沒有重複鍵。重複鍵在解析階段就已被「後者覆蓋」,文字 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,記憶體與耗時都不划算。超大檔案更適合串流處理或分片比較。