JSON 比對工具
您輸入的內容不會被儲存、記錄或分享。
貼上兩份 JSON,即可依路徑列出所有新增、刪除與變更的值。比對會忽略鍵的順序,只回報實際的差異。
使用方式
- 將原始 JSON 貼到左側。
- 將要比對的 JSON 貼到右側。
- 差異會列在下方,並標示完整路徑。
比較的運作方式
兩份文件都會先解析,再從根節點開始同步走訪,比較兩邊相同路徑上的內容。物件依鍵對應,所以調換鍵的順序不會造成差異;陣列則依索引對應。每一項差異都會歸為三種之一:新增,表示該路徑只存在於右側;刪除,表示只存在於左側;變更,表示兩側在該路徑都有值,但值不同。
型別改變也算是變更,而且一旦出現就值得仔細查看。原本是數字 42 的欄位變成字串 "42",通常在寬鬆的用戶端仍能運作,接著在下游某個需要拿它做運算的地方失敗。原本是物件的欄位變成只含一個物件的陣列也是一樣,這在依條件組合回應的程式碼中是很常見的意外。
為什麼陣列依索引比較
這是對結果影響最大的設計決策,所以值得說清楚。在含 200 個元素的陣列開頭插入一個元素,依索引比較會回報 200 項變更,因為插入點之後的每個元素索引都變了。這並不是出錯,只是差異比較回答的問題和你心裡想的不一樣。
另一種做法是尋找最長共同子序列並回報插入,文字的逐行 diff 就是這樣做的。但對 JSON 來說,這不見得比較好。它必須判斷清單中的兩個物件何時算同一個物件,這需要一個你沒有指定、工具也猜不到的鍵;一旦猜錯,就會給出一個看似篤定卻誤導人的答案。依索引比較雖然粗略,但絕不會捏造不存在的對應關係。
實務上,如果清單確實被重新排序過,請先將兩邊依識別碼排序再比較。
null、不存在的鍵,以及如何讀路徑
一邊存在且值為 null、另一邊完全不存在的鍵,會被回報為新增或刪除,而不是變更,因為這是兩種不同的狀態,程式碼也會以不同方式處理。不存在通常代表該欄位從未被設定;null 通常代表刻意設定為「沒有值」。把其中一種當作另一種使用的 API 會造成錯誤,而這個工具能讓你看出你實際送出的是哪一種。
路徑要當成從根節點出發的路線來讀:user.roles[2] 是 user 物件裡 roles 陣列的第三個元素。這個語法刻意與你在 JavaScript 中存取該值時的寫法相同,也與 JSONPath 表示式足夠接近,可以直接貼到大多數接受 JSONPath 的工具中。
找出真正變動的回歸測試流程
JSON 差異比對最常見的用途,是確認一次部署沒有改動到不該改動的東西。先抓取現行版本的回應,再從候選版本抓一次,然後比對。困難的從來不是比對本身,而是雜訊,因為一份回應裡通常都有每次都會合理變動的欄位。
比對之前先把這些欄位移除或正規化:自動產生的識別碼、時間戳記、request id、帶有效期的簽章 URL,以及任何回報耗時的欄位。寫一小段腳本走訪整份文件,把這類欄位換成固定的佔位字串,就能把兩百處差異的比對結果,縮成只剩真正重要的那一處。
對於 API 沒有承諾順序的地方也要比照辦理。如果標籤清單每次呼叫回來的順序都不同,請先用穩定的鍵值把兩邊排序;否則以索引為基礎的比對會把每個元素都報成有變動,把唯一真正的差異淹沒在其中。
相等、完全相同與等價
兩份文件可以在好幾種意義上「相等」,而工具必須挑一種。位元組層級的相等最嚴格,也最沒有用,因為只要重新排版就會失效。結構上的相等——不論空白與鍵的順序,只要是相同的鍵搭配相同的值——就是這裡執行的比對。語意上的等價則是第三件事,取決於結構描述。
數字是結構比對最尷尬的地方。JSON 不區分整數與浮點數,所以對剖析器來說 1、1.0 和 1e0 是同一個值;產生端要輸出哪一種,可能會隨函式庫版本而改變,實際上卻什麼有意義的東西都沒變。小數點後的尾隨零會在一次來回轉換中消失,也是同樣的道理。
字串則可能看起來一模一樣,位元組卻不同,因為 Unicode 允許同一個字元有不只一種編碼方式:一個帶重音的字母可以是單一碼點,也可以是基本字母加上結合附加記號。比對前先正規化成 NFC,就能消除這一類假差異;只要文字來自一個以上的來源,這一步都值得做。
什麼時候差異比對不是對的工具
比對兩個範例,只能告訴你這兩個範例的事。檢查回應是否符合某條規則是另一回事,該用的工具是結構描述:文件用 JSON Schema,端點用 OpenAPI。結構描述會聲明 id 是整數、email 要符合某個樣式,而且對每一次回應都成立,不只對你剛好抓到的那一組。
實務上兩者是搭配使用的。持續整合裡的結構描述驗證,負責攔截結構上的漂移——欄位型別變了、必要屬性不見了;而差異比對回答的是比較窄的問題:在調查過程中,這兩份特定的酬載之間到底變了什麼。
契約測試則更進一步,讓消費端公布自己依賴哪些內容,於是生產端的流水線會在某項變更真的會弄壞實際客戶時失敗,而不是在它只是跟舊樣本不一樣時失敗。
讀懂路徑並直接跳過去
這裡回報的路徑,讀起來就是一條從根節點出發的路線:user.roles[2] 是 user 物件裡 roles 陣列的第三個元素。這是刻意設計成你在 JavaScript 裡為了取到那個值會實際輸入的運算式,所以只要把文件放進一個變數,路徑就能直接貼進主控台使用。
同樣的形式也很接近 JSONPath,也就是多數日誌與 API 工具都接受的查詢語言,只是運算式開頭多了一個錢字號:$.user.roles[2]。命令列處理工具 jq 使用類似但沒有錢字號的記法,所以 .user.roles[2] 可以從檔案或資料流中取出同一個值。
面對非常龐大的文件時,真正有用的輸出是這條路徑,而不是差異本身。一旦知道差異落在哪裡,就能用 jq 或 HTTP 客戶端的篩選器,只把兩邊的那一段子樹拉出來,這比任何完整文件的檢視方式都更好讀。
比對大到讀不完的文件
數 MB 的回應在分析資料匯出與批次端點上很常見,整份拿去比對只會產生沒人用得了的結果。有效的做法是先縮減再比對:挑出你在意的子樹,只投影出正在討論的欄位,並依識別碼把集合排序。
先數數量是很有用的第一步。如果一邊有 4,812 筆紀錄、另一邊有 4,811 筆,真正要問的是哪一筆不見了,而那是識別碼的集合比對,不是文件差異比對——兩份排序過的 id 清單加上一次以行為單位的 diff,幾秒鐘就能給出答案。
當酬載真的很大、而且比對必須反覆執行時,命令列管線會勝過瀏覽器:用 jq 把兩個檔案正規化成排序過的緊湊單行,再交給 diff。兩種做法都完全在本機執行,但命令列不需要維持任何頁面的流暢度。
常見問題
- 鍵的順序會影響比對結果嗎?
- 不會。物件是依鍵而非位置比對,因此調換順序不會產生差異。陣列則依索引比對,因為陣列的順序具有意義。
- 我的資料會上傳到伺服器嗎?
- 不會。比對完全在您的瀏覽器中以 JavaScript 執行,兩份資料都不會離開您的裝置,因此可安全比對正式環境的 API 回應。
- user.roles[2] 這樣的路徑代表什麼?
- 它標示差異的位置:user 物件中 roles 陣列的第三個元素。點號分隔物件的鍵,方括號則表示陣列索引。
- null 與鍵不存在有什麼不同?
- 兩者會分開回報。若一邊的鍵值為 null,另一邊完全沒有該鍵,會顯示為新增或刪除,而非變更。
最後更新:
請提供操作步驟、預期結果及實際結果。請移除機密與個人資料。
