DevKitLab Logo DevKitLab
JSON / 差分 / 開発ワークフロー

2 つの JSON ファイルを比較して、本当に変わった部分だけを見る方法

JSON を行単位で比較すると、整形やキー順の違いが本当の変更を隠します。ターミナル、エディター、コード、ブラウザーでの比較方法を整理します。

変更されたメールフィールドをハイライトした 2 つの JSON の構造化比較

API を一つ変更し、意図した範囲だけが変わったことを確かめたい。追加されたフィールドが一つ、値が変わった箇所が一つ、それ以外は同じであるべきです。ところが旧レスポンスと新レスポンスを普通の diff に入れると、実際には二つしか変えていないのに画面は赤と緑の行で埋まります。

コードが壊れているとは限りません。比較ツールが別の問いに答えているだけです。一般的な diff はテキストを比べます。しかし同じ意味の JSON でも、キー順、インデント幅、圧縮済みか整形済みかによって表記は変わります。必要なのはデータとしての比較です。ここではターミナル、エディター、コード、ブラウザーでそれを行う方法と、それぞれの限界を見ていきます。

ここで使う例

/users レスポンスの二つの版を比べます。新しい版では配列の先頭に一人追加され、別の利用者のメールドメインのタイプミスが直っています。変更はそれだけです。

// before.json
{
  "users": [
    { "id": 1, "name": "Ada", "email": "ada@example.com", "role": "admin" },
    { "id": 2, "name": "Grace", "email": "grace@example.com", "role": "user" }
  ]
}
// after.json — user 3 を追加し、Grace のメールドメインを修正
{
  "users": [
    { "id": 3, "name": "Linus", "email": "linus@example.com", "role": "user" },
    { "id": 1, "name": "Ada", "email": "ada@example.com", "role": "admin" },
    { "id": 2, "name": "Grace", "email": "grace@example.org", "role": "user" }
  ]
}

まず一つ、誤解をほどく

「テキスト diff では JSON を扱えない」という言い方は正確ではありません。現在の行単位 diff は、挿入や削除をかなりうまく見つけます。上の二つのファイルを git diff に渡すと、追加された行と変更された行はきれいに検出されます。

@@ -1,6 +1,7 @@
 {
   "users": [
+    { "id": 3, "name": "Linus", "email": "linus@example.com", "role": "user" },
     { "id": 1, "name": "Ada", "email": "ada@example.com", "role": "admin" },
-    { "id": 2, "name": "Grace", "email": "grace@example.com", "role": "user" }
+    { "id": 2, "name": "Grace", "email": "grace@example.org", "role": "user" }
   ]
 }

問題は挿入そのものではありません。テキストだけが変わり、データは変わっていないときに起こります。

テキスト diff がノイズを作る場面

実際の JSON では、次の三つが頻繁に起こります。

整形。 片方が圧縮済み、もう片方が整形済みなら、同じデータでも全体が変更のように見えます。

@@ -1 +1,6 @@
-{"users":[{"id":1,"name":"Ada"},{"id":2,"name":"Grace"}]}
+{
+  "users": [
+    { "id": 1, "name": "Ada" },
+    { "id": 2, "name": "Grace" }
+  ]
+}

キー順。 JSON オブジェクトのキー順には意味がありませんが、テキスト diff は順序の違いを知りません。別のサービスや別言語のシリアライザーが同じレコードを出力し、キー順だけが変わることがあります。

@@ -1,5 +1,5 @@
 {
+  "email": "ada@example.com",
   "name": "Ada",
-  "role": "admin",
-  "email": "ada@example.com"
+  "role": "admin"
 }

Ada のデータは変わっていません。配列順も同様です。同じ要素が移動しただけなのに、行単位 diff では後半が削除と追加に見えることがあります。保存時の整形、キーをソートするシリアライザー、環境ごとの出力順の違いは、確認したい一箇所の変更を埋もれさせます。

ターミナルでは正規化してから比べる

最も手軽なのは、比較前に両方を正規化することです。jq -S は JSON を解析し、キーをソートして統一された形式で出力します。空白とキー順だけの差は消えます。

diff <(jq -S . before.json) <(jq -S . after.json)

上の「整形だけ違う」「キー順だけ違う」ケースでは、正規化後に両者が同じバイト列になるため出力はありません。本当の変更では、追加ユーザーとメールの編集だけが残ります。色付きの表示が必要なら、追跡されていない二つのファイルも比べられる git diff --no-index before.json after.json を使えます。

ただし jq -S が並べ替えるのはキーだけで、配列要素は並べ替えません。配列内の要素が移動した場合は、構造を理解する比較が必要です。

エディターでそのまま比べる

二つのファイルをすでに開いているなら、ターミナルに移る必要はありません。多くのエディターは二つのファイルを選んで比較でき、同期スクロールする左右表示と行内ハイライトを出せます。これは依然としてテキスト比較なので整形と順序の問題は残りますが、形式が近い二つのファイルを素早く確認するには最短です。

コードやテストで比較する

テストやスクリプトに入れるなら、解析済みの構造を比べます。Python の deepdiff は配列順を無視できます。

from deepdiff import DeepDiff
import json

before = json.load(open("before.json"))
after = json.load(open("after.json"))

print(DeepDiff(before, after, ignore_order=True))

ここで注意が必要です。ignore_order=True では、要素はまず内容で対応付けられます。追加された Linus は問題ありませんが、Grace のオブジェクト自体は変化しています。内容だけでは同じ人物だと判断できず、「一件削除して一件追加」と報告されることがあります。

安定した識別子を渡し、同じエンティティかどうかを先に判定します。

from deepdiff.helper import CannotCompare

def same_user(left, right, level=None):
    try:
        return left["id"] == right["id"]
    except (TypeError, KeyError):
        raise CannotCompare() from None

diff = DeepDiff(before, after, ignore_order=True, iterable_compare_func=same_user)

同じ id の要素が先に対応付けられるため、Grace のメールは値の変更、Linus は追加として扱われます。DeepDiff の位置表記は root[...] ですが、$.users[2].email のような JSONPath が必要なら、その形式を出力するツールやライブラリを使います。テストでは差分を表示するより、差分が空であることを検証するのが一般的です。

assert DeepDiff(expected, actual, ignore_order=True) == {}

重要なのは言語ではありません。二つの値を解析し、構造をたどり、型付きの変更を返すことです。配列を位置ではなく識別子で対応付けると、「配列全体が変わった」は「一つの項目が変わった」に戻ります。

変更には参照できる住所がある

構造化比較は、行番号だけでは得にくいデータ内の位置を与えてくれます。ライブラリごとに記法は異なりますが、対話ツールでは JSONPath として $.users[2].email のように示せます。深くネストした設定や大きな payload では、「214 行目が変わった」より「$.users[2].email が X から Y になった」の方がレビュー、障害報告、テストで使えます。行番号はファイルを示し、パスはデータを示します。

手早く目で確認したいとき

コードの構造化比較は自動化に向きますが、日常的には二つの payload を貼り付けて慎重に確認したいだけのことも多いでしょう。毎回配列の識別関数を書くのは手間ですし、長い文字列のどの断片が変わったかも分かりにくいものです。

JSON Diff に両方を貼り付けると、配列オブジェクトの idkeynameuuid_id などを安定キーとして優先します。そのため先頭に Linus を入れても、後ろのユーザーはずれません。Ada と Grace は対応したまま、user 3 は追加、Grace は変更として表示され、文字列中の com → org だけも確認できます。横の一覧には JSONPath ごとの変更が並びます。

JSON Diff:変更後には追加ユーザーが表示され、Grace のメールは変わった断片だけが強調され、横のパネルに JSONPath ごとの二件の変更が並ぶ
id による配列の対応付け、文字列内の単語単位ハイライト、JSONPath で示す変更一覧。

配列がスカラーの一覧だったり、オブジェクトに安定キーがなかったりすると、位置による比較へ戻ります。キー順を無視する切り替え、変更されていない領域の折りたたみも利用できるため、大きな設定でも必要な部分に集中できます。すべてブラウザー内で処理されるので、外部サービスへ貼り付けたくない本番レスポンスやトークンの payload を確認する際にも便利です。パイプラインの jq や構造化比較ライブラリを置き換えるものではなく、準備なしの目視確認に向く手段です。

実際に使う場面

  • API の変更レビュー。 エンドポイントの前後の応答を貼り付け、ネストや長い文字列を含めて意図したフィールドだけが変わったかを確認します。
  • 設定の編集。 tsconfig.jsonpackage.json、lint 設定、機能フラグなどではキー順の変更がノイズになりがちです。意味として比較すると本当の変更が残ります。
  • フィクスチャとシードデータ。 テスト用データの前後を比べ、想定外のケースまで触っていないことを確かめます。

まだ有効な JSON ではない場合

ここまでの方法は、両方が解析できることを前提にしています。編集途中には末尾カンマ、閉じ忘れた括弧、途中まで貼り付けた payload があり得ます。パーサーは最初の構文エラーで止まるので、構造化比較はできません。まず jq . file.json または python -m json.tool file.json で最初の行と列を見つけます。二つの未完成テキストが近く、どこで分かれたかを先に見たいなら、行単位の テキスト比較 を使います。構文を直してから構造化比較へ戻りましょう。

状況別の選び方

状況選ぶもの
整形やキー順だけが異なるターミナルの jq -Sdiff
配列要素の順番が変わった構造化比較、または JSON を理解するツール
ファイルがすでにエディターで開かれているエディターのファイル比較
テストやスクリプトで比較する識別子で対応付けた deepdiff、または構造化比較ライブラリ
長い文字列も含めて目で確認したいブラウザーの JSON Diff
入力がまだ解析できないjq . / python -m json.tool で場所を直し、テキスト比較

誤りは diff を使うことではありません。データを、テキストしか知らない比較に渡し、全体が赤くなったことから大きな変更があったと判断することです。データをデータとして比べ、配列を識別子で対応付け、変更をパスで説明すれば、本当に行った変更だけが見えてきます。