OpenAI Structured Outputsを使うと、一見すると正しい JSON Schema に見えるものでも、API 側で拒否されることがあります。
よくあるエラーは、次のようなものです。
Invalid schema for response_format ...
'additionalProperties' is required to be supplied and to be false.
これは通常、プロンプトの問題でも、モデルが JSON の生成に失敗したという問題でもありません。リクエストで渡しているスキーマ自体が、OpenAI Structured Outputs が期待する JSON Schema のサブセットに合っていないため、拒否されています。
OpenAI Structured Outputs では、object 型のスキーマで定義していないプロパティを明示的に禁止する必要があります。
"additionalProperties": false
最小の修正
スキーマに次のような object が含まれている場合は、
{
"type": "object",
"properties": {
"title": { "type": "string" },
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
}
},
"required": ["title", "priority"]
}
次のように変更します。
{
"type": "object",
"properties": {
"title": { "type": "string" },
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
}
},
"required": ["title", "priority"],
"additionalProperties": false
}
普通の JSON Schema の感覚だとハマる理由
通常の JSON Schema では、次のスキーマは、
{
"type": "object",
"properties": {
"title": { "type": "string" }
}
}
「この object には title だけが入る」という意味ではありません。
これは単に、次のような意味です。
titleというプロパティがある場合、その値は string でなければならない。
たとえば、次の object もこのスキーマでは通る場合があります。
{
"title": "Fix login bug",
"unexpected_field": "this is still allowed"
}
なぜなら、properties は既知のフィールドに対する制約を定義するだけで、定義していないキーを禁止するわけではないからです。properties に列挙されていないフィールドを拒否するには、次の指定が必要です。
"additionalProperties": false
違いは、次のように覚えるとわかりやすいです。
properties
= どのフィールドを知っているか
required
= その既知のフィールドが必ず出現するか
additionalProperties: false
= それ以外のフィールドを許可しない
エラーになる最小例
次のスキーマには additionalProperties: false がありません。
{
"type": "object",
"properties": {
"title": {
"type": "string"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
}
},
"required": ["title", "priority"]
}
OpenAI Structured Outputs では、この object 型のスキーマは不完全です。定義していないフィールドが来たときにどう扱うかが指定されていないためです。
そのため、API から additionalProperties を指定し、かつ false にする必要がある、というエラーで拒否されることがあります。
修正後:additionalProperties: false を追加する
{
"type": "object",
"properties": {
"title": {
"type": "string"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
}
},
"required": ["title", "priority"],
"additionalProperties": false
}
入れ子になった object でハマるパターン
たとえば、次のスキーマにはまだ問題があります。
{
"type": "object",
"properties": {
"task": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
}
},
"required": ["title", "priority"]
}
},
"required": ["task"],
"additionalProperties": false
}
入れ子になった task object では、未定義のキーがまだ禁止されていません。
内側の object も修正します。
{
"type": "object",
"properties": {
"task": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
}
},
"required": ["title", "priority"],
"additionalProperties": false
}
},
"required": ["task"],
"additionalProperties": false
}
目安はシンプルです。
"type": "object"を書いたら、その同じ object 型のスキーマに"additionalProperties": falseがあるか確認する。
配列の中にも同じ問題が隠れる
次のスキーマには、task object の配列があります。
{
"type": "object",
"properties": {
"tasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"done": {
"type": "boolean"
}
},
"required": ["title", "done"]
}
}
},
"required": ["tasks"],
"additionalProperties": false
}
items の中の object でも、未定義のキーがまだ禁止されていません。
修正後は次のようになります。
{
"type": "object",
"properties": {
"tasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"done": {
"type": "boolean"
}
},
"required": ["title", "done"],
"additionalProperties": false
}
}
},
"required": ["tasks"],
"additionalProperties": false
}
デバッグするときは、items、$defs、入れ子になった properties の中にある object も含めて、すべての object を確認します。
required と混同しない
次の部分は、
"required": ["title", "priority"]
これらのフィールドが必ず出現する、という意味です。
一方で、次の部分は、
"additionalProperties": false
properties に列挙されていないフィールドを許可しない、という意味です。
OpenAI Structured Outputs では、すべてのフィールドを required に含める必要があります。実質的に optional として扱いたい値は、null を使って表現します。
たとえば、次のように書きます。
{
"type": "object",
"properties": {
"title": {
"type": "string"
},
"due_date": {
"type": ["string", "null"]
}
},
"required": ["title", "due_date"],
"additionalProperties": false
}
これは次の意味です。
due_date は必ず存在する。
ただし、その値は string または null でよい。
Pydantic や Zod を使う場合は、生成後のスキーマを確認する
Pydantic の場合、model が実行時に追加フィールドをどう扱うかと、OpenAI が受け付ける JSON Schema は同じものではありません。Pydantic の extra 設定は検証時の挙動を制御します。一方で OpenAI は、リクエストで渡された最終的な JSON Schema を検証します。
Zod の場合は、変換モードが重要です。Zod の JSON Schema ドキュメントでは、通常の z.object() は additionalProperties: false を出力する一方、{ io: "input" } を指定した変換では出力しない、と説明されています。z.looseObject() はこれを出力せず、z.strictObject() は常に出力します。
Python でも TypeScript でも、安全なデバッグ手順は同じです。
最終的なスキーマを出力する。
すべての "type": "object" を探す。
それぞれの object に "additionalProperties": false があるか確認する。