【ITニュース解説】Why 'diff' fails on JSON (and how to compare two JSON files properly)
2026年10月20日に「Dev.to」が公開したITニュース「Why 'diff' fails on JSON (and how to compare two JSON files properly)」について初心者にもわかりやすく解説しています。
ITニュース概要
一般的なdiffコマンドはJSONのキー順や空白を誤って変更と認識し、不正確。JSONの意味を正しく比較するには、jqでの整形・ソート、Pythonの専用モジュール、JavaScriptの関数など、データ形式に合った方法を使おう。
ITニュース解説
システム開発では、APIのレスポンスや設定ファイルなど、構造化されたデータをJSON形式で扱うことが非常に多い。JSONは人間が読みやすく、プログラムも扱いやすい優れたデータ形式だ。しかし、二つのJSONファイルが「同じ」であるかを確認する際に、一般的なファイル比較ツールであるdiffコマンドを使うと、しばしば大量の差分が表示されて混乱することがある。これは、diffコマンドがテキスト(文字)の並びを比較するのに対し、JSONはデータの「意味」を表現する形式であり、見た目のテキスト表現と意味的な内容が異なる場合があるためだ。
diffコマンドは、ファイル内の文字や行の並び順に厳密だ。しかし、JSONの標準仕様であるRFC 8259によると、JSONオブジェクト内のキー(例:"name")とその値のペアの順序は意味を持たないと定義されている。つまり、{"a":1,"b":2}と{"b":2,"a":1}は、キーの並び順が異なっていても、JSONとしては全く同じ内容を指す。また、JSONデータ間の空白文字(スペース、改行、タブなど)もデータの内容には影響を与えない。diffはこれらのJSONのルールを理解できないため、キーの順序が変わったり、単に整形(インデント)が変わったりしただけで、それらを「変更点」として検出してしまう。これにより、本当にデータが更新された箇所を見つけるのが非常に困難になるのだ。一方で、JSON配列は要素の並び順に意味があるため、[1,2]と[2,1]は異なる配列と見なされる。
JSONファイルを「意味」で正しく比較し、本当に変更された箇所だけを検出するには、いくつかの効果的な方法がある。
一つ目の方法は、jq -Sコマンドとdiffコマンドを組み合わせるやり方だ。jqはJSONデータをコマンドラインで操作するための強力なツールだが、-Sオプションを付けると、JSONオブジェクト内のキーを再帰的にアルファベット順にソートし、さらに整形された形で出力してくれる。この機能を利用して、比較したい二つのJSONファイルをそれぞれjq -S . filename.jsonで処理し、整形・ソートされた結果をdiffコマンドで比較する。例えば、diff <(jq -S . a.json) <(jq -S . b.json)のように実行できる。この方法で、キーの順序や空白文字の違いによる不要な差分表示をなくし、意味のある変更点だけを効率的に抽出できる。ただし、配列の順序は依然として比較対象となり、また、変更箇所が行単位でしか表示されないため、具体的なデータパス(例: $.address.city)でどこが変わったのかを知ることはできないという限界がある。
二つ目の方法は、Pythonなどのプログラミング言語を利用することだ。Pythonの標準ライブラリであるjsonモジュールを使ってJSON文字列をPythonの辞書やリストなどのオブジェクトに読み込むと、それらのオブジェクトを==演算子で比較するだけで、JSONの「意味」に基づいた比較が行われる。Pythonの辞書はキーの順序を意識しないため、JSONのキー順序の違いは無視される。また、数値の1と1.0は同じ値と見なされる。配列の順序はJSONの仕様通り重要視されるため、[1,2]と[2,1]は異なるものとして扱われる。もし、単に同じかどうかの真偽だけでなく、具体的な変更点やそのパスを知りたい場合は、DeepDiffのような外部ライブラリを使うと、より詳細な差分情報を得られる。
三つ目の方法は、JavaScriptでパスベースの差分検出関数を自作することだ。これは約30行程度の再帰関数として実装でき、二つのJSONオブジェクトを比較して、意味的に異なる部分だけを検出し、その変更箇所を$.tags[1]や$.address.cityのような具体的なデータパスとともにリストアップしてくれる。この関数は、オブジェクトのキー順序や空白を無視し、もしデータ型自体が変わっていた場合(例えば数値が文字列になっていた場合)も、それを明確に「型変更」として報告する。この方法は、APIのテストなどで特定のデータ項目がどのように変化したかを厳密に追跡したい場合に非常に有用だ。JavaScriptの新しい機能であるObject.hasOwnを使うことで、より正確なキーの存在チェックが可能になる。
JSONの比較で特に注意が必要なのが配列の扱方だ。もしAPIが返すユーザーリストなどで、リスト内の要素の順序がAPI呼び出しごとに変わってしまう場合、単純に要素を位置で比較すると、リスト全体が「変更された」と誤って報告されてしまうことがある。しかし、実際には一部のユーザー情報だけが変わったのかもしれない。このような問題を解決するには、配列の各要素に一意な識別子(例えばid)がある場合、そのidをキーとして配列をオブジェクト(連想配列)に変換してから比較するという方法が有効だ。これにより、IDごとにデータが「追加された」「削除された」「変更された」といった、より意味のある差分情報を得られる。ただし、配列の要素に一意なキーがない場合(単純な数値や文字列の配列など)は、配列の順序がデータの一部であると判断するか、比較前に両方の配列をソートするといった工夫が必要となる。
実際のシステム運用でJSON比較を行う際には、さらにいくつかの「落とし穴」に注意する必要がある。一つは大きな数値だ。JavaScriptではJSON内のすべての数値を64ビット浮動小数点数として扱うため、JavaScriptが正確に扱える最大の整数値を超える大きなIDなどがJSONに含まれていると、その値が丸められてしまい、異なるIDが「同じ」と判断されてしまう可能性がある。このようなデータは、文字列として比較するか、BigIntをサポートする特殊なJSONパーサーを使うべきだ。次に、重複キーの問題がある。JSONの仕様ではオブジェクト内のキーは一意であるべきだが、多くのJSONパーサーは重複するキーを許容し、通常は最後に出現した値を採用する。これにより、比較対象の一方でデータが意図せず失われ、気づかないうちに不正確な比較が行われる可能性がある。また、数値の1と1.0のように表記が異なるだけの値は、JavaScriptやPythonでは等しいと見なされるが、テキストdiffや厳密なスキーマ検証では異なるものとして扱われる場合がある。最後に、型変更も重要な問題だ。例えば、APIレスポンスのあるフィールドが以前は数値の5だったのに、変更後に文字列の"5"になっていた場合、これはデータ型の重大な変更であり、アプリケーションに予期せぬエラーを引き起こす可能性がある。パスベースの差分関数はこのような型変更も適切に報告してくれる。
自動化されたテストや継続的インテグレーション(CI)パイプラインの中でJSON比較を行う場合は、上記で説明したようなコードベースの比較方法を用いるべきだ。一方で、一時的に手動で二つのJSONファイルの内容を素早く確認したいだけであれば、キー順序や空白を無視して意味的に比較してくれるオンラインのJSON比較ツールも便利に利用できる。多くのツールはブラウザ内で処理が完結するため、機密性の高いデータをインターネット上にアップロードする心配も少ない。
まとめると、JSONファイルを比較する際には、単なる文字の並びとしてではなく、そのデータが持つ「意味」を理解することが何よりも重要だ。従来のdiffコマンドはJSONの特性を理解できないため、誤解を生みやすい。代わりに、jq -Sとdiffの組み合わせ、Pythonのjsonモジュールによるオブジェクト比較、またはJavaScriptによるパスベースの差分検出といった方法を活用することで、真に意味のある変更点だけを正確に把握できる。さらに、配列の順序に関する問題への適切な対処や、大きな数値、重複キー、データ型変更といった実運用上の注意点に気を配ることで、システムエンジニアとしてより信頼性の高いデータ管理と変更点の把握が可能になるだろう。