本文へスキップ
jsonbeautifiers
日本語

JSONの差分比較

構造で比較するので、書式変更やキーの並び替えは差分になりません。

変更前
変更後

貼り付けたものがブラウザの外に出ることはありません。 connect-src の許可リストにより、これは約束ではなくブラウザによる保証になっています。 自分で確かめる

これは2つのドキュメントを行単位ではなく構造で比較します。片方を整形し直しても、キーを並べ替えても、インデントを変えても、差分は空のままです。どれもデータを変えていないからです。

JSONに対するテキスト差分がほぼ役に立たないのは、まさにそれが理由です。別のライブラリで再シリアライズすると、実際には何も変わっていないのに全行が変更として報告されます。

何を差分とみなすか

4種類の変更があり、それぞれ正確な位置を指すJSON Pointerとともに報告されます。

追加
右側にあって左側にないキーまたは要素。
削除
左側にあって右側にないもの。
変更
両方にあり、同じ型で値が異なるもの。
型の変化
両方にあり、型が異なるもの。分けて扱う価値があります。数値が文字列になっているのは、たいていデータの変更ではなくシリアライズのバグだからです。

配列をどう比較するか、そしてそれがなぜ重要か

配列は、あらゆる構造差分にとって難しい部分であり、唯一の正解はありません。

位置ごとの比較は、形の決まったタプルには正しく、リストには壊滅的です。1000要素の配列の先頭に1つ挿入すると、それ以降のすべての位置が変更として報告され、本当の差分1件が偽の差分1000件に埋もれます。

このツールは代わりに最長共通部分列のパスを実行するので、挿入は挿入として報告されます。その計算量は2つの長さの積に比例するため、しきい値を超えると位置ごとの比較に切り替え、そのことを注記に明示します。黙って劣化することはありません。

意図的に差分としないもの

キーの順序
仕様上、JSONのオブジェクトは順序を持ちません。同じメンバーが違う順序で並んだ2つのドキュメントは、同じドキュメントです。
空白とインデント
パーサーにとって無意味なので、ここでも見えません。
0と-0
Object.isでは区別されますが、JSONの数値としては同一です。

How to do this in code

コードでの構造比較。

sh jq

覚えておくべきは1行目です。先にキーをソートするだけで、テキスト差分のノイズはほとんど消えます。

# Sort keys recursively, then diff the text. Cheap and
# surprisingly effective for small documents.
diff <(jq -S . a.json) <(jq -S . b.json)

# Keys present in one and not the other
jq -n --slurpfile a a.json --slurpfile b b.json \
  '($a[0] | paths) - ($b[0] | paths)'
py Python
from deepdiff import DeepDiff

diff = DeepDiff(a, b, ignore_order=True)
print(diff)

# Standard library only, for a shallow comparison
changed = {k for k in a.keys() | b.keys() if a.get(k) != b.get(k)}
js JavaScript

JSON Patchは、JSONの差分を送るための標準フォーマットです。そのパスは、このツールが報告するのと同じRFC 6901のポインタです。

import { compare } from 'fast-json-patch';

// RFC 6902 JSON Patch: a list of operations that turns a into b
const patch = compare(a, b);
// [{ op: 'replace', path: '/user/name', value: 'Priya' }]
go Go

go-cmpはGoのテストにおける定番で、ネストした構造でも読みやすい出力を出します。

import "github.com/google/go-cmp/cmp"

if d := cmp.Diff(a, b); d != "" {
    t.Errorf("mismatch (-want +got):\n%s", d)
}

よくある質問

キーを並べ替えても差分が出ないのはなぜですか?
RFC 8259がオブジェクトを順序のないコレクションとして定義しているからです。実際にはどのパーサーも挿入順を保持しますが、それに依存する差分はデータではなくシリアライズを検査していることになります。
タイムスタンプのようなフィールドを無視できますか?
できます。パスを無視すると比較から完全に外れます。requestIdとgeneratedAtだけが違う2つのAPIレスポンスを比較したいときは、この方法を使います。
JSON Pointerとは何ですか?
RFC 6901のことで、/users/0/emailのようなスラッシュ区切りのパスです。キーの中では2文字だけエスケープが必要で、~は~0に、/は~1になります。ここで報告される変更にはすべてこれが付くので、プログラムから位置を指定できます。