この記事について
この記事は、生成AIを活用した自動生成フローで作成しています。Python公式のjsonモジュール仕様を参照し、架空のJSON文字列で重複キーの扱いを説明します。検証ステータス:📘 公式仕様確認済み・掲載コードの実機実行は未確認
PythonのJSONで同じキーが2回出たら?上書きと重複検出を試す
JSONの設定を読み込んだのに、先に書いた値が消えたように見えることがあります。たとえば架空の設定に "mode": "test" と "mode": "live" が同居していたら、どちらが使われるでしょうか。Python標準ライブラリの json.loads() が通常の辞書へ変換すると、同名キーの最後の値が残ります。重複していた事実を、普通の辞書だけから復元することはできません。
この挙動は「JSONの読み込みに失敗した」という意味ではありません。入力の書き方が曖昧でも、プログラムが最後の値を採用して処理を続けることが問題なのです。設定値やAPIの受信データを安全に扱いたいなら、辞書に変わる前に同じ名前が登場していないか確認する方法があります。
対象環境と試す範囲
| 項目 | 内容 |
|---|---|
| 言語 | Python 3系、標準ライブラリ json |
| 用意するもの | Pythonの対話環境または練習用スクリプト |
| 入力 | 記事内の架空JSON文字列のみ |
| ファイル・通信 | ファイル読書き、ネットワーク通信なし |
| 確認状況 | Python公式仕様を参照。掲載例の実機実行は未実施 |
実際の設定ファイルや業務データを直接書き換える必要はありません。最初は文字列だけを使い、値がどう変化するか観察します。既存システムの設定にこの方法を取り入れる際は、受信側だけでなく送信側の仕様や、重複時にエラーとすべきかという契約も確認してください。
まずは小さな実験:最後の値が残る
次のコードでは同じ mode が2回登場します。表示される値は、Python公式ドキュメントに記載された既定動作に基づく期待結果です。
import json
raw = '{"mode":"test","mode":"live","retry":2}'
data = json.loads(raw)
print(data)
print(data["mode"])
期待される辞書は {'mode': 'live', 'retry': 2} で、2行目の値は live です。最初の test は辞書の同じキーに上書きされたため残りません。ここで json.loads() が例外を投げないことも重要です。構文として読めるJSONでも、アプリケーションが期待する一意性を満たすとは限りません。
たとえば再試行回数が 2 と 20 の二重定義なら、誤設定を見逃す原因になります。監視画面に最終値しか出なければ、入力元のどこで重複したか調べにくくなります。特に複数の設定を結合する処理や、テンプレートを手で編集する工程では、同じキーを二度書いてしまう可能性を考えておきたいところです。
なぜ読み込み後には見つけにくいのか
JSONオブジェクトは名前と値の組を並べた構造です。一方、Pythonの通常の辞書では同じキーに二つの値を同時に保持しません。変換の途中で後の値が採用されると、元の並びにあった重複は失われます。
次の図は、元のJSON文字列から辞書になるまでの情報の変化を示します。左側に二つある mode が、右側では一つになる点に注目してください。
flowchart TD
A["JSON文字列: mode=test, mode=live"] --> B{"読み込み方法"}
B -->|通常のloads| C["辞書: mode=live"]
B -->|object_pairs_hook| D["組の列: mode=test, mode=live"]
D --> E["重複を検査して拒否できる"]
C --> F["元の重複は判別しにくい"]
図の右側の方法なら、辞書へまとめる前に名前と値の組を受け取れます。Pythonの object_pairs_hook は、JSONオブジェクトの組を順番どおりに受け取るための仕組みです。単に dict を作るのではなく、独自の関数で重複を検査できます。ここではファイルの保存や削除をせず、異常な入力を見つけたら例外にするだけにします。
重複を見つけたら明示的に止める
次の関数は受け取った組を順番に確認し、すでに登録したキーが現れたら ValueError を送出します。最後まで重複がなければ辞書を返します。
import json
def reject_duplicate_keys(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f"duplicate JSON key: {key}")
result[key] = value
return result
raw = '{"mode":"test","mode":"live","retry":2}'
try:
parsed = json.loads(raw, object_pairs_hook=reject_duplicate_keys)
print(parsed)
except ValueError as exc:
print(type(exc).__name__, str(exc))
この例では duplicate JSON key: mode を含むエラー表示が期待されます。関数が呼ばれる時点で pairs はキーと値の組の並びです。重複を見つけたら辞書への変換を完了させずに停止するため、後の値を黙って採用しません。
ここでの設計判断は「どの値を優先するか」ではなく「入力が曖昧なら拒否するか」です。重要な設定やアクセス制御に関係する値なら、勝手に先勝ち・後勝ちを決めるより、設定作成者へ修正を求める方が安全な場面があります。ただし、既存システムが重複を許容している場合、突然この検査を導入すると互換性に影響するため、まず受信データの実態を調査してください。
一つ条件を変えて違いを見る
先ほどの文字列から二つ目の mode を削除し、'{"mode":"test","retry":2}' に変更してみます。同じ検査関数でも、今度は辞書が返ることが期待されます。変えたのは重複の有無だけです。検査処理のコードを変えずに正常系と異常系を比較できるため、関数が何を禁止しているのか理解しやすくなります。
もう一つ、キーの大文字小文字を変えた "mode" と "Mode" も考えましょう。この関数はPythonの文字列キーをそのまま比較するため、別名として扱います。業務上は同じ項目と見なしたい場合、アプリケーション側で正規化規則を別途決める必要があります。ただし、JSONの標準的なキー比較と、業務独自の大文字小文字無視を混同してはいけません。
さらに、入れ子のオブジェクトではどうなるでしょうか。object_pairs_hook は各JSONオブジェクトを処理するときに呼ばれるので、内側に重複があっても検出対象になります。一方、異なるオブジェクトに同じ mode が一つずつあることは重複エラーではありません。重複の範囲はJSON文書全体ではなく、それぞれのオブジェクトの内部です。
失敗時の切り分け:重複以外にも原因がある
json.loads() 自体が JSONDecodeError を出した場合、まず引用符、カンマ、波括弧などJSON構文を確認します。たとえば末尾カンマを付けたJSONはPythonの標準デコーダーでは許されません。重複キーの検出関数へ到達する前に構文エラーとなる場合もあります。構文エラーと重複キーエラーを同じ「JSONが壊れた」で片付けず、入力のどこを修正すべきか分けて記録しましょう。
object_pairs_hook と object_hook は似た名前ですが、受け取る情報が異なります。後者は通常の辞書へ変換された後のオブジェクトを受け取るため、元の同名キーを検査する用途には適しません。両方を指定した場合は object_pairs_hook が優先されるという公式仕様もあります。重複を見つけたいときは、どの段階で関数が呼ばれるかを意識することが肝心です。
巨大なJSONや不審な外部入力では、重複検出だけで安全になるわけではありません。極端に大きいデータや深い入れ子は、処理時間やメモリ消費の問題を引き起こします。受信サイズの上限、タイムアウト、許容する項目、値の型、スキーマ検証は別途必要です。今回の関数は重複名という一つの問題に対する対策であり、万能なJSON検証器ではありません。
業務で使うなら、検出結果をどう扱うか
たとえば設定をレビューする作業では、最初に元のJSONを保管し、重複検査で問題があれば取り込みを保留し、修正後に再検査する流れが考えられます。元の入力を上書きしてからエラーを調べるのではなく、変更前の内容を追跡できるようにすることが重要です。公開用ログへJSON全体を出すと秘密値を漏らす危険があるため、実務ではエラー種別や安全な項目名など必要最小限の情報に絞ってください。
APIで受け取ったJSONにも同じ考え方が使えます。ただし、すべてのサービスが重複キーを拒否するとは限りません。送信側と受信側で解釈が違えば、署名検証、キャッシュ、監査など別の処理と不一致が起こり得ます。重要な連携では「同じキーが複数回あったらエラー」とインターフェース仕様に明記し、テストデータにも正常例と重複例を用意するのがよいでしょう。
単純な設定を手作業で確認するだけなら、元文字列をレビューして修正する方法でも十分な場合があります。一方、毎日大量のJSONを自動処理するなら、受信入口で重複を機械的に拒否する価値が高まります。どちらを選ぶにしても、読み込み後の辞書だけを見て「入力に重複はなかった」と結論づけないことが、この実験から得られる大事な発見です。
まとめ
Pythonの json.loads() は通常、重複キーの最後の値を採用します。重複を見つけたい場合は、辞書になる前の組を受け取る object_pairs_hook を使います。入力の一意性、構文の正しさ、値の型、ファイルサイズや機密性は別々の確認事項です。安全な小さな例で違いを理解してから、業務の入力契約に合わせて適用してください。
公式資料
Python公式:json — JSON encoder and decoder — 重複名の既定動作、
object_pairs_hook、デコーダーの仕様を確認する資料。RFC 8259:The JavaScript Object Notation (JSON) Data Interchange Format — オブジェクトの名前は一意であることが望ましいという相互運用上の注意を確認する資料。

