JSON Toolkit

additionalProperties を閉じないと、Schema 検証は検証になっていない

要点:検証を通過してもデータが clean とは限りません。Pydantic は未知フィールドを無視し、Zod は剥離し、Ajv は素通し—— hallucination された引数やタイプミスのキーが静かに通り抜けます。本稿は沈黙の受入れの3形態を示し、オブジェクトを閉じ、強制変換を制御し、ネスト全層で締める完全なレシピを提供します。

Intermediate約 8 分で読めます2026-11-02 公開2026-11-02 更新
余分なフィールドを含む JSON が検証を通る様子

結論:デフォルトでは、主要なバリデータは未宣言フィールドを「拒否」ではなく「無視」します。additionalProperties: false を書かなければ、検証通過は「宣言したフィールドに問題がない」ことしか保証しません。

バリデータの最も危険な挙動はエラーを出すことではなく、エラーを出さないことです。タイプミスのキーやモデルが捏造した引数は検証層を静かに通過し、ビジネス層で爆発します。

一:「通過」する検証の実例

ツール呼び出し引数の契約に2つのフィールドだけを宣言した schema を考えます:

{
  "type": "object",
  "properties": {
    "order_id": { "type": "string" },
    "amount": { "type": "number" }
  },
  "required": ["order_id", "amount"]
}

{"order_id": "A1", "amount": 50, "reasn_code": "damaged"} を渡します——3番目のキーは reason_code のタイプミスです。Ajv のデフォルト設定での検証結果は通過。余分な reasn_code は拒否も除去もされず、そのままビジネスロジックに入ります。

これは Ajv のバグではありません。JSON Schema 仕様では additionalProperties のデフォルトは true です。「どのフィールドがあるか」は宣言しましたが「そのフィールドしかあってはならない」とは宣言していないので、バリデータは仕様通りに動いています。

二:実行時の3種類の「沈黙」

同じ欠落が言語ごとに異なる形で現れます:

実行時 未宣言キーのデフォルト挙動 結果
Ajv(JS、デフォルト) そのまま通す 汚れたデータが業務層へ
Zod z.object(TS) 剥離(strip) フィールドが消え、下流のオブジェクトが短くなる
Pydantic v2(Python) 無視(extra="ignore") 同上。ログに痕跡なし

剥離は素通しより隐蔽的です。dev.to の Pydantic vs Zod 比較記事はこれを明確に述べています:両ライブラリはデフォルト状態で同じように——静かに——失敗します。

三:LLM ツール呼び出しが問題を拡大する

LLM のツール呼び出し引数はモデルが生成します。フィールド名を捏造し、"3" を数値として渡し、必須欄に曖昧な値を入れます。各プラットフォームは引数を JSON Schema で定義しますが、デフォルトでは schema のクローズを強制しません——strict モードは明示的に有効化する必要があり、文法制約はモデルの直接出力にしか適用されません。

防線はローカルに築くしかありません:

{
  "order_id": 4411,
  "refund_amount_usd": 50,
  "reason_code": "damaged",
  "urgent": "yes"
}

schema は最初の2フィールドのみ宣言。オブジェクトを閉じなければ後の2キーは素通り;閉じれば reason_code と urgent が拒否され、エラーは /reason_code パスを指します。

四:閉じ方:3つのエコシステム対照

Ajv:オブジェクトノードに additionalProperties: false を追加。

import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true });

const schema = {
  type: "object",
  properties: { order_id: { type: "string" }, amount: { type: "number" } },
  required: ["order_id", "amount"],
  additionalProperties: false,
};

const validate = ajv.compile(schema);
validate({ order_id: "A1", amount: 50, reasn_code: "x" }); // false
validate.errors[0].instancePath; // "/reasn_code"

Zod:z.object を z.strictObject に、または .strict() をチェーン。 Pydantic:ConfigDict(extra="forbid") を設定。

共通ルールは層ごとに閉じること。ネストされた各オブジェクトにも個別に additionalProperties: false が必要です。一層でも漏れれば、その層が新しい穴になります。

五:強制変換の方針も決める

Pydantic の lax モードは "5" を 5 に変換します。方針は3つ:

  1. 厳格モード:型が違えば拒否(Pydantic strict=True、coerce なし Zod、Ajv はそもそも強制変換なし);
  2. 制御付き変換:明示的な変換のみ許可(Zod z.coerce.number())+範囲指定 .positive().max(10000);
  3. 完全放行:内部の既知データソースのみ。

LLM 引数のような信頼できない入力には 1 か 2 を選び、テストに書き込みます。意味が曖昧なフィールド(単位のない金額など)は refund_amount_usd のように単位をフィールド名に置きます。

六:ネストと配列のチェックリスト

チェック点 做法
トップレベル additionalProperties: false
各ネスト object 同様に閉じる
配列要素 items 内のオブジェクトも閉じる
列挙と範囲 enum、minimum/maximum を補う
生成器の出力 キーワードを手動で補完

properties の宣言は required の宣言ではありません。生成器出力の詳細は《自動生成された JSON Schema がなぜ実データを拒否するのか》を参照。

七:拒否後のエラーをフィールドに向ける

allErrors: true で全越界フィールドを一度に報告し、verbose: true で具体パスを取得。instancePath の翻訳は《JSON Schema エラーメッセージの読み方》を参照。構文レベルの問題は schema 検証前に /parse で弾きます。

よくある質問

additionalProperties: false は厳しすぎますか?

契約次第です。自分で定義する入力なら閉じるべき;サードパーティの拡張余地があるデータはコアのみ閉じ、拡張領域を明示的に分けます。

Zod の strip の方が安全では?

strip は下流を守り、呼び出し側を害します。エラーは早く明示的に現れるべきで、strip はその逆です。

strict モードがあればローカル検証は不要?

必要です。schema 自体が未収口の可能性があり、文法制約がツール引数を覆わない場合もあるため、ローカルでの再検証はほぼ無コストです。

閉じた後、旧データが大量に落ちる?

それこそ暴露すべき問題です。allErrors で一覧化し、無害なキーは properties に追加、有害なものはデータ修正。通すためにスイッチを戻さないこと。

一行で済む兜底は?

ありませんが、最小の動作は:全 schema 定義を検索し、各 type: "object" に additionalProperties: false が付いているか確認することです。