JSON diff ツールの選択:テキスト差分、構造差分、可視化
要点:テキスト差分はどこが異なるかを見つけ、構造差分は内容が等価かを決めます。正しい層を選べば、キー順をインシデントと誤認しません。

要点:テキスト差分は変更を発見し(jq -S で先にキーを並べ替え)、構造差分は等価性を判定し(配列を主キーで整列し)、可視化はそれを説明・レビューするだけです。CI では三つの層を分けて保ちましょう。
二つの JSON 文書が異なって見えることは、内容が異なることを意味しません。ツールを選ぶ前に正しい比較層を選びましょう。
1. 三種類の「diff」は同じものではない
多くのチームは「JSON を比較する」を単一の行為として扱い、場面ごとに同じ問題にぶつかります。三つの層を分けると役立ちます。
| 層 | 比較するもの | 明らかにするもの | 典型的な用途 |
|---|---|---|---|
| テキスト差分 | 生のバイトと行 | インデント、キー順、空白、改行 | 設定ドリフト、コードレビュー |
| 構造差分 | パース済みのオブジェクトと配列 | 追加/削除されたキー、変更された値、配列要素の変更 | API 回帰、データ移行 |
| 可視化差分 | 差分の提示方法 | 変更がどの階層に属するか、影響範囲の大きさ | 人間のレビュー、チーム議論 |
テキスト差分は依存がなく、速く、Git と自然に統合します。弱点は JSON の意味論を何も知らないことです。完全に等価な二つの文書も、キー順やインデントが違えば赤と緑に光ります。構造差分は逆で、まずパースしてから比較するので、値が単に移動したことを認識します。しかし正しい配列整列戦略が必要で、さもないと 1 件の挿入が全体の書き直しと誤読されます。
可視化は第三のアルゴリズムではありません。他の二層の出力を描画し、人間が差分が予期されたものかを素早く確認できるようにします。人間の判断には良いですが、自動チェックの代替としては貧弱です。層を混ぜると、永久に赤い CI パイプラインと、それを無視することを学んだチームができがちです。
2. テキスト差分:まず 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} は同じ値です。しかしテキスト差分はすべての行を変更済みとマークします。先に並べ替えれば、赤いブロックはずっと実際の問題に近づきます。
jq -S はキーだけを並べ替え、配列は並べ替えないことに注意してください。JSON では配列の順序に意味があり、[1,2] は [2,1] と同じ値ではないので、ツールはその判断をしません。配列が概念的に順序なしの集合なら、比較前に自分で主キーで並べ替えてください。
覚えておく価値のある詳細がもう一つ。diff は差異を見つけると終了コード 1 を返しますが、ファイルの欠落や権限エラーも非ゼロを生みます。CI で、すべての非ゼロコードを「内容が変わった」と扱わないでください。パスの打ち間違いがデータ変更として報告されます。より安全なパターンは、まずファイルを確認してから差異を扱うことです。
if ! diff -u a.sorted.json b.sorted.json; then
echo "JSON の内容が異なります" >&2
exit 1
fi
delta がテキスト差分を読みやすくする
生の diff -u 出力でも動きますが、長い行と深くネストした JSON は読むのが疲れます。delta は差分フィルタで、シンタックスハイライト、行番号、より明確な背景色を加えます。
diff -u a.sorted.json b.sorted.json | delta
もともと Git のページャーとして生まれたので、git diff を引き継ぐこともできます。一日に十件の設定差分を読む人にとって、こうしたツールはアルゴリズムの正確さではなく読む速度を改善します。もう一つの一般的な選択は git diff --no-index --word-diff で、単語単位で比較し、非常に長い一行に収められた圧縮 JSON によく効きます。
警告が一つ。美化は偽の差異を除去しません。キー順が正規化されていなければ、delta は雑音を綺麗にするだけです。正しい順序は常に、まず正規化、次に装飾です。
3. 構造差分:jsondiffpatch は意味を理解する
二つのデータが意味的に等価かを問うときは構造差分を使います。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 を直接歩けます。差異がなければ undefined、あればパスごとの記述です。テキスト差分と違い、インデント、キー順、1e3 対 1000 のような数値の綴りに乱されません。
同じくらい重要なのは、構造差分の出力がパッチのワークフローに流せることです。jsondiffpatch は patch と reverse を提供するので、delta を元のオブジェクトに適用し、後で取り消せます。注意点が一つ。自動生成された delta は手書きの JSON Patch と同じ形式ではなく、変換には手間がかかります。より広い比較の方法はJSON の差分と比較を参照してください。
4. キー順による偽の差異を除去する方法
キー順は煩わしいだけでなく、誤った結論を生みます。調査時は三つの手順に従います。
- 比較前に
jq -Sか同等のツールでキー順を再帰的に正規化する。 timestamp、requestId、traceIdなど毎回変わるフィールドを除去する。- 重複キーがないことを確認する。重複キーはパース時に「最後が勝つ」で解決されるので、テキスト差分には見え、構造差分には見えません。
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
この流れは設定ドリフトによく合います。ステージングと本番を同じ正規化に通し、残ったものが実際に説明を要する差異です。フィルタ規則はバージョン管理されたスクリプトに保ち、記憶から打ち直さないでください。ツールチェーンの編集とスクリプトの側面はエディタ、コマンドライン、自動化を参照してください。
5. 配列:インデックスで整列するか、id で整列するか
配列は JSON 比較で最も誤りやすい部分です。既定のインデックス整列では、先頭に 1 レコード挿入するとそれ以降のすべてがずれ、ツールはすべてを変更済みとマークします。ツールは壊れておらず、整列戦略が間違っています。
| 整列 | 機能する条件 | 強み | リスク |
|---|---|---|---|
| インデックス | 配列の順序が業務上の意味を持つ | 単純。追加フィールド不要 | 1 件の挿入が連鎖的な誤検知を生む |
| 主キー | 要素が一意の id を持つ集合 | 挿入、削除、移動を検出 | 安定した一意キーが必要 |
| 最長共通部分列 | 順序が重要だが挿入は許される | 純粋なインデックスより堅牢 | 説明が難しく、コストが高い |
jsondiffpatch は objectHash で主キーを宣言し、比較前に配列要素を索引化します。
const instance = create({
objectHash: (item: { id?: string; sku?: string }) => item.id ?? item.sku,
});
素の jq なら、先に並べ替えてからテキスト差分を使えます。
jq -S 'sort_by(.id)' items.json > items.sorted.json
id 整列のトレードオフは、位置の変化が差異として数えられなくなることです。レコードが位置 2 から位置 5 へ移動したことを業務が知る必要があるなら、明示的な順序フィールドも比較してください。集合の中身だけに頼らないでください。
6. 可視化差分:人のために作られた層
構造差分の出力は機械向けで、会議向けではありません。変更を他人に説明する必要があるときは、可視化の方が効果的です。
- VS Code の差分エディタはファイル単位のレビューに適し、並列ペインとインラインハイライトがあります。
- JSON Crack や JSON Hero のようなツリービューアは、変更がどの階層に属するかを示します。
jsondiffpatchは HTML フォーマッタを同梱し、delta を折りたたみ可能なリッチビューとして描画します。
可視化の価値は文脈です。テキスト差分は 88 行目が変わったと言い、ツリービューは 88 行目が data.items[3].price に属すると言います。しかし可視化は通常、文書全体をブラウザかデスクトップアプリに読み込みます。本番のペイロードを開く前に、ツールがローカルで計算するかサーバーにアップロードするかを確認してください。
7. diff を CI に入れる三つの実践
第一に、比較前に正規化する。インデントを固定し、キーを再帰的に並べ替え、揮発性フィールドをフィルタしてから diff を実行します。さもないと CI は雑音で繰り返し失敗します。
第二に、意味論に基づいて整列を選ぶ。配列が集合なら主キーで整列し、並びならインデックス整列を保ちます。すべての API に同じ規則を適用しないでください。
第三に、テキストではなく値を表明する。テストでパース済みオブジェクトを比較する方が、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
差異を長期追跡する必要があるなら、正規化スクリプトと期待ファイルを一緒にコミットしてください。一度きりの調査なら、後で一時スクリプトを削除し、静かに業務ロジックの第二実装にならないようにします。
8. ツール選択表
| 場面 | 第一選択 | 代替 | 重要な注意 |
|---|---|---|---|
| たまのファイル比較 | VS Code の差分 | diff -u |
先に整形して雑音を除去 |
| 設定ドリフト | jq -S + diff |
delta |
揮発性フィールドをフィルタ |
| API レスポンスの回帰 | jsondiffpatch |
deepdiff |
配列を主キーで整列 |
| チームへの変更説明 | JSON Crack / JSON Hero | HTML フォーマッタ | データがアップロードされるか確認 |
| 非常に大きなファイル | jq でサブツリー抽出 |
jless で閲覧 |
ツリー全体をメモリに載せない |
| プリコミットチェック | git diff --no-index |
pre-commit | 終了コードを明示的に扱う |
唯一最良の JSON diff ツールはなく、現在の層に合うツールだけがあります。テキスト差分は変更を発見し、構造差分は等価性を判定し、可視化は変更を説明します。三つの層を分けて保つ方が、すべてをこなす一つのツールを探すよりはるかに信頼できます。ブランチをまたぐ競合の扱いはJSON と Git のマージに続きます。
FAQ
jq -S は配列の順序も変えますか?
いいえ。-S はオブジェクトのキーだけを並べ替え、配列は元の順序のままです。JSON では配列は順序を持つので、ツールは [1,2] を [2,1] にしません。配列が概念的に順序なしの集合なら、sort_by で明示的に並べ替えてください。
構造差分では 1 と 1.0 は同じ値ですか?
ほとんどのパーサーは両方を同じ数値にするので、構造差分は通常等しいとみなし、テキスト差分は異なるとマークします。等価性の判断には構造差分、ファイルがバイト単位で同一かにはテキスト差分を使ってください。
diff の出力をそのまま JSON Patch に使えますか?
そのままでは使えません。diff ツールは何が異なるかを報告し、JSON Patch は順序付きの操作一覧を記述します。形式も意味論も異なります。自動生成パッチは配列インデックスを参照しがちで、並行下で陳腐化します。適用前に test 操作を足してください。
数十 MB の JSON ファイルはどう比較しますか?
まず関心のあるサブツリーを抽出します。たとえば jq '.data.items'。次に主キーで並べ替えて比較します。再帰的な diff のためにツリー全体をメモリに載せないでください。非常に大きなファイルはストリーミングやチャンク比較で扱う方がよく、メモリコストは別途評価が必要です。
オンライン差分ツールはデータを漏らしますか?
実装次第です。純粋なフロントエンドツールはブラウザで計算し、データをアップロードしません。サーバー描画のツールは内容をバックエンドに送り、ログに入るかもしれません。本番設定や顧客のペイロードを比較する前に、ページがローカルで計算することを確認し、必要ならネットワークを切って一度試してください。
テキスト差分と構造差分はどう決めればよいですか?
二つのファイルがどこで異なるかを素早く見るには、jq -S でキーを並べ替えて diff -u を実行します。二つの文書が意味的に等価かを決めるには、jsondiffpatch のような構造差分を使い、配列を主キーで整列します。テキスト差分は依存がなく Git と統合し、構造差分はインデント、キー順、数値の綴りに乱されません。
CI が diff のすべての非ゼロ終了コードを差異と扱うべきでないのはなぜですか?
diff は差異を見つけると 1 で終了しますが、ファイルの欠落や権限エラーも非ゼロを返します。すべての非ゼロコードを「内容が変わった」と扱うと、パスの打ち間違いがデータ変更として報告されます。より安全なパターンは、まずファイルを確認し、それから差異を別に扱うことです。
数十 MB のファイルをブラウザで diff すべきですか?
いいえ。まず jq で関心のあるサブツリーを抽出し、次に主キーで並べ替えて比較します。再帰的な diff のためにツリー全体をメモリに載せると、メモリと時間がはるかに多くかかります。非常に大きなファイルはストリーミングやチャンク比較で扱う方がよいです。