2回書いた limit が、読み込み後には1個になった。Pythonの json.loads() で {"limit": 1, "limit": 100} を読むと、残るのは 100 だ。
LLMの出力をツールの引数に渡すなら、重複キーは辞書化前に止めたい。消えたほうの値は、後から追えないんだよね。
自分は strict=True も試して、同じ結果で止まった。名前から期待した厳しさと、チェック範囲が違った。
Pythonのjson.loadsで値が消えるところ
CPython 3.14.8、標準ライブラリで実行した。入力は手書きの検証用JSONだ。
import json
raw = '{"limit": 1, "limit": 100}'
print('default:', json.loads(raw))
print('strict:', json.loads(raw, strict=True))
print('object_hook:', json.loads(raw, object_hook=lambda obj: obj))
default: {'limit': 100}
strict: {'limit': 100}
object_hook: {'limit': 100}
整数かどうかを調べても、100 は通る。元の重複は辞書から復元できない。
標準ライブラリのソースも読んだ。Python版の JSONObject() は、キーと値のペアをため、通常は dict(pairs) に変換する。同じキーの後の値が残り、続いて object_hook を呼ぶ。
strict が制御するのは、文字列内の制御文字を許すかどうか。同ファイルの JSONDecoder の説明でも確認した。重複キーの拒否は含まれない。名前が紛らわしい。
object_pairs_hookで辞書化の直前に止める
object_pairs_hook には辞書化前のペアが順番どおり渡る。重複を見つけたら例外にする。
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f'duplicate key: {key!r}')
result[key] = value
return result
cases = {
'normal': '{"limit": 1}',
'same_value': '{"limit": 1, "limit": 1}',
'nested': '{"args": {"limit": 1, "limit": 100}}',
'escaped': r'{"limit": 1, "\u006cimit": 100}',
'separate': '[{"limit": 1}, {"limit": 100}]',
}
for name, text in cases.items():
try:
value = json.loads(text, object_pairs_hook=unique_object)
print(name, 'OK', value)
except ValueError as exc:
print(name, 'NG', exc)
続けて実行した結果。
normal OK {'limit': 1}
same_value NG duplicate key: 'limit'
nested NG duplicate key: 'limit'
escaped NG duplicate key: 'limit'
separate OK [{'limit': 1}, {'limit': 100}]
同じ値の重複も拒否する。
入れ子の args にもフックが呼ばれる。自前の再帰処理は不要だ。エスケープは先に解釈されるので、limit と \u006cimit も同じキーとして検出できた。
配列の別要素にある limit は通る。result はオブジェクトごとに作り直す。
ツールへ渡す手前に置く
流れは「生のJSON → 重複キー検査 → 引数の型・値の検証 → ツール実行」にする。
この関数が担当するのは重複キーだけ。許可する引数名や数値の上限、NaN などの非標準の数値の拒否は別に扱う。
SDKから辞書を受け取る場合、生の文字列を読む箇所に差し込む。既存の object_hook と併用すると object_pairs_hook が優先されるので、元の変換処理の移植も確認したい。
例外に出すのはキー名まで。入れ子のパスは付けていない。
LLMの引数処理では、パース後の検証に目が行きやすい。5ケースで、辞書化の前に消える情報も試験に含められる。SDKに変換を任せる箇所が増えるほど、生のJSONを検査できる位置の確認が効いてくると思う。既存の引数テストに重複キーの入力を1件足せば、今の処理がどう扱うかを確かめられる。