てもとツール
ガイド

JSONを比べると差分だらけになる原因|見かけの差分と消える差分

同じデータなのに、2つのJSONを比べると差分だらけになるのはなぜか。キーの順番・空白・文字のエスケープ・数値の書き方・配列の並びの5つを、Python・PHP・JavaScriptで実際に出力した例で整理します。値で比べても消えてしまう差(大きな整数と重複キー)も説明します。

公開読了目安 約5分

この記事の要点

  • 同じデータでも、書き出す言語や設定で JSON の字面は変わる
  • キーの順番・空白・エスケープ・数値の書き方は、値が同じでも行の差分になる
  • 配列の並びは、データによって意味がある場合と無い場合がある
  • 大きな整数と重複キーは、値で比べても差が消えることがある
  • 字面をそろえてから比べるか、値で比べるかを先に決める
目次

2 つの JSON を行で比べて差分だらけになるのは、多くの場合、中身ではなく書き方が違うからです。 キーの順番・空白・文字のエスケープ・数値の書き方は、値が同じでも別の行になります。

見かけの差分に埋もれると、本当に変わった 1 項目を見落とします。 この記事では、見かけの差分が生まれる 5 つの原因と、逆に消えてしまう差を順に整理します。

同じデータでも、書き出す言語で字面が変わる

まず、同じ 3 項目を 3 つの言語で JSON に書き出した結果を並べます。 どれも既定の設定のまま出力しました。

Python   {"name": "佐藤", "score": 80.0, "url": "https://a/b"}
PHP      {"name":"佐藤","score":80,"url":"https:\/\/a\/b"}
JavaScript {"name":"佐藤","score":80,"url":"https://a/b"}

3 行とも、名前は「佐藤」、点数は 80、URL は同じ値です。 それでも、行として比べると 3 行すべてが違います。

Python は区切りの後ろに空白を入れ、日本語を \u の形にします。 PHP はスラッシュを \/ にし、80.0 を 80 と書きます。 JavaScript は日本語をそのまま出します。

データを作る側の言語が変わったり、設定が 1 つ変わったりするだけで、ファイルの差分は大きくなります。

見かけの差分を生む 5 つの原因

値が同じなのに行の差分になる原因は、次の 5 つに分けられます。 上の例に出てきたものから順に見ていきます。

キーの順番

JSON のオブジェクトは、キーの順番に意味を持ちません。 {"a":1,"b":2} と {"b":2,"a":1} は同じ値です。

ところが行で比べると、並びが違うだけで別の行になります。 データベースから取り出した順や、プログラムが組み立てた順で、並びは簡単に入れ替わります。

空白・改行・字下げ

JSON では、記号の前後の空白・タブ・改行は意味を持ちません(RFC 8259 §2)。 1 行に詰めた JSON と、字下げした JSON は同じ値です。

Python の例では、字下げをしないときは ", " と ": " のように空白を入れて書きます(Python ドキュメント)。 空白の入れ方が違うだけで、全行に印が付きます。

文字のエスケープ

"佐藤" と "佐藤" は、エスケープを解くと同じ文字列です(RFC 8259 §8.3)。 "https:\/\/a\/b" と "https://a/b" も同じです。

Python は日本語を既定で \u の形にし、PHP はスラッシュも既定でエスケープします(両者のドキュメント)。 人が読むと別物に見えるので、いちばん気づきにくい見かけの差分です。

数値の書き方

80、80.0、8e1 は、どれも同じ 80 です。 PHP は既定で 80.0 を 80 と書き、Python は 80.0 のまま書きます。 PHP マニュアルの JSON_PRESERVE_ZERO_FRACTION の説明にある挙動です。

小数点以下が 0 の数値は、出力する言語が変わるたびに書き方が揺れます。 値は変わっていないので、差分として扱う必要はありません。

配列の並び

配列は、ほかの 4 つと事情が違います。 JSON の配列は、並び順に意味があるデータです。

ランキングや手順の一覧なら、並びが変わったことは本物の差分です。 一方、ユーザーの一覧のように id で区別できるデータなら、並びの違いは見かけの差分にすぎないこともあります。

どちらかは、JSON だけでは決まりません。 データの意味を知っている人が、比べ方を選ぶ必要があります。

5 つの原因と、そろえ方の対応表

5 つの原因を、行で比べたときの見え方と、見かけの差分を消す方法で並べます。

原因行で比べたときの見え方見かけの差分を消す方法
キーの順番同じ行が別の位置に出るキーを並べ替えてから比べる/値で比べる
空白・改行・字下げほぼ全行に印が付く同じ設定で整形し直す/値で比べる
文字のエスケープ日本語や / を含む行に印が付くエスケープを解いて書き直す/値で比べる
数値の書き方小数点以下が 0 の数値の行に印が付く値で比べる(表記は別に確かめる)
配列の並び要素が入れ替わった範囲に印が付く並びに意味が無いときだけ、キーで突き合わせる

表の左から 2 列目は、その原因があるときの見え方です。 逆向きには使えません。

同じ見え方は複数の原因から生まれます。 ほぼ全行に印が付いたからといって、原因が字下げだけとは限りません。

値で比べても消えてしまう差

見かけの差分とは反対に、比べる道具によっては本物の差が消えることもあります。 値で比べる道具が、JavaScript の JSON.parse と同じ方法で数値やキーを読んでいる場合です。

大きな整数の末尾

JSON.parse は、数値を倍精度の浮動小数点数として持ちます。 そのため、次の 2 つは読み込んだ時点で同じ値になります。 12345678901234567890 と 12345678901234567891 です。

RFC 8259 は、倍精度で扱う実装どうしで値が確実に一致する整数を、絶対値が 2 の 53 乗未満の範囲としています。 20 桁の ID のような値は、この範囲の外にあります。

同じキーが 2 回ある JSON

{"a":1,"a":2} のように同じキーが 2 回ある JSON は、JSON.parse で読むと後の値 2 だけが残ります。 前の 1 は、比べる前に消えます。

RFC 8259 は、キーを重複させないよう求めています。 重複したときの扱いは実装ごとに違い、多くの実装は最後の組だけを使うと書かれています。

比べる前に決めておくこと

JSON を比べる前に、「字面の違いも知りたいか」「値の違いだけを知りたいか」を決めておきます。

設定ファイルの書式まで管理しているなら、字面の違いにも意味があります。 その場合は、整形とキーの並べ替えをそろえてから行で比べます。

API の返す中身が変わったかを知りたいなら、値で比べるのが近道です。 JSON比較は値で比べ、書き方だけ違う数値や重複キーは差分とは別に知らせます。

まとめ

JSON を比べて差分だらけになる原因は、キーの順番・空白・文字のエスケープ・数値の書き方・配列の並びの 5 つです。 前の 4 つは値が同じなので、そろえてから比べるか、値で比べれば消えます。

配列の並びは、データによって本物の差分にも見かけの差分にもなります。 大きな整数と重複キーは、値で比べる道具でも消えることがあるので、字面を確かめる手段を残しておきます。

参考・出典

本記事は上記の公的機関・公式情報源を参考にしています。最新の情報は各リンク先をご確認ください。

あわせて読みたい

同じテーマ・関連ツールの記事です。