このエラーが発生する原因
Pydanticモデルを定義し、JSONスキーマに変換して、OpenAIのファンクションコーリングAPIに渡すと、以下のエラーが返ってきます:
ValidationError: Invalid JSON for OpenAI function call
ここで問題が発生するケースは2つあります。OpenAIのAPIがスキーマを完全に拒否するケースと、呼び出し自体は成功するものの、レスポンスをPydanticオブジェクトに変換する際に、構造が期待と一致せずパースに失敗するケースです。どちらの場合も、原因はほぼ必ずPydanticが生成するものとOpenAIが実際に期待するものの不一致にあります。
デバッグの手順
ステップ1 — 送信している実際のスキーマを出力する
推測せず、スキーマをダンプして内容を確認してください。
import json
from pydantic import BaseModel
from typing import Optional, List
class SearchParams(BaseModel):
query: str
max_results: int = 10
category: Optional[str] = None
# Pydantic v2
schema = SearchParams.model_json_schema()
print(json.dumps(schema, indent=2))
出力結果をJSON Formatter & Validatorに貼り付けると、構造上の問題が即座にハイライトされます。完全にブラウザ側で動作するため、データが外部に送信されることはありません。
ステップ2 — OpenAIが実際に受け取った内容を確認する
デバッグロギングを有効にして、生のAPIリクエストをキャプチャします:
import openai
import logging
logging.basicConfig(level=logging.DEBUG)
openai.log = "debug"
デバッグ出力の中からfunctionsまたはtoolsのペイロードを探してください。それがOpenAIが検証している正確なスキーマです。
ステップ3 — 根本原因を特定する
よくある原因は以下の通りです:
.model_json_schema()の代わりに.schema()を使用している(Pydantic v2の破壊的変更)- ネストされたモデルの
$defs/$ref— OpenAIは内部参照を解決しない OptionalフィールドによるanyOfの生成 — 一部のOpenAIモデルはこのパターンを拒否するadditionalProperties: falseの欠如 — strictモードで必須- ネストされたプロパティの
titleなど、余分なPydanticメタデータフィールド
解決方法
修正1 — 使用しているPydanticバージョンに合った正しいスキーマメソッドを使う
Pydantic v2では.schema()が.model_json_schema()に名称変更されました。これは多くのマイグレーションでつまずく点です。古いメソッドはv2でも動作しますが、非推奨の警告が表示され、互換性のない構造が返されることがあります。
# Pydantic v1
schema = MyModel.schema()
# Pydantic v2 — こちらを使用する
schema = MyModel.model_json_schema()
修正2 — ネストされたモデル参照をフラット化する
Pydanticモデルを別のモデル内にネストすると、生成されたスキーマに$defsと$refポインターが含まれます。OpenAIのファンクションコーリングAPIはこれらを内部で解決せず、未解決の参照として認識してエラーになります。
from pydantic import BaseModel
from typing import List
class Item(BaseModel):
name: str
quantity: int
class Order(BaseModel):
items: List[Item]
shipping_address: str
# $defsが含まれる — OpenAIで問題になる
raw_schema = Order.model_json_schema()
# $defsをインライン展開してフラット化する
def inline_refs(schema: dict, defs: dict) -> dict:
if '$ref' in schema:
ref_key = schema['$ref'].split('/')[-1]
return inline_refs(defs[ref_key], defs)
result = {}
for key, value in schema.items():
if key == '$defs':
continue
elif isinstance(value, dict):
result[key] = inline_refs(value, defs)
elif isinstance(value, list):
result[key] = [inline_refs(i, defs) if isinstance(i, dict) else i for i in value]
else:
result[key] = value
return result
defs = raw_schema.get('$defs', {})
flat_schema = inline_refs(raw_schema, defs)
print(json.dumps(flat_schema, indent=2))
修正3 — anyOfを生成するOptionalフィールドを処理する
Pydantic v2ではOptional[str]が{"anyOf": [{"type": "string"}, {"type": "null"}]}としてレンダリングされます。OpenAIのstrictモードはanyOf内のnull型を拒否します。対処法は2つあります。フィールドごとにオーバーライドするか、スキーマ全体からnullを一括で除去するかです。
from pydantic import BaseModel, Field
from typing import Optional
class SearchParams(BaseModel):
query: str
category: Optional[str] = Field(
default=None,
json_schema_extra={"type": "string"} # anyOfをプレーンな型でオーバーライド
)
あるいは、スキーマを後処理してanyOfからnullを除去します:
def strip_null_anyof(schema: dict) -> dict:
"""{anyOf: [T, null]} を単純にTに置換する"""
if 'anyOf' in schema:
non_null = [s for s in schema['anyOf'] if s.get('type') != 'null']
if len(non_null) == 1:
return {**non_null[0], **{k: v for k, v in schema.items() if k != 'anyOf'}}
return {k: strip_null_anyof(v) if isinstance(v, dict) else v for k, v in schema.items()}
修正4 — strictモード用にadditionalProperties: falseを追加する
strictなファンクションコーリングには1つの厳格なルールがあります。スキーマ内のすべてのオブジェクトは、追加プロパティを明示的に禁止する必要があります。Pydanticはデフォルトでこれを追加しません。
def add_strict_flags(schema: dict) -> dict:
if schema.get('type') == 'object':
schema['additionalProperties'] = False
for value in schema.get('properties', {}).values():
add_strict_flags(value)
return schema
strict_schema = add_strict_flags(MyModel.model_json_schema())
修正5 — instructorを使って手動作業をなくす
instructorライブラリはまさにこの問題のために作られました。スキーマユーティリティ関数を1つも書かずに、$refのフラット化、anyOfのクリーンアップ、strictモードフラグの設定を自動で処理してくれます:
pip install instructor
import instructor
from openai import OpenAI
from pydantic import BaseModel
client = instructor.from_openai(OpenAI())
class UserInfo(BaseModel):
name: str
age: int
user = client.chat.completions.create(
model="gpt-4o",
response_model=UserInfo,
messages=[{"role": "user", "content": "Extract: John is 30 years old"}]
)
print(user) # UserInfo(name='John', age=30)
スキーマの調整も後処理ヘルパーも不要です。同じ問題に複数回直面するなら、導入を検討する価値があります。
修正を検証する
実際のAPIを呼び出す前に、jsonschemaを使ってスキーマをローカルでテストします:
import jsonschema
import json
schema = flat_schema # 処理済みのスキーマ
# スキーマが有効なJSON Schemaか確認する
try:
jsonschema.Draft7Validator.check_schema(schema)
print("Schema is valid")
except jsonschema.SchemaError as e:
print(f"Schema error: {e.message}")
# サンプルペイロードに対してテストする
sample = {"query": "test", "max_results": 5}
try:
jsonschema.validate(sample, schema)
print("Sample payload validates correctly")
except jsonschema.ValidationError as e:
print(f"Payload error: {e.message}")
両方のチェックが通れば、APIを呼び出してください。ValidationErrorが出なければ修正は成功です。
得られた教訓
- **まずスキーマを出力する。**バリデーターに貼り付けて内容を確認してください。ほとんどのエラーはすぐに明らかになり、APIを触る必要すらありません。
- **Pydantic v1 → v2のマイグレーションはよくある落とし穴です。**アップグレード直後にこのエラーが発生した場合、
.schema()を.model_json_schema()に変えるだけで解決できる可能性が高いです。 - **OpenAIのstrictモードは文字通りstrictです。**すべてのフィールドを宣言し、すべてのオブジェクトに
additionalProperties: falseを付け、ツリー内に$refを一切含めないことが必要です。 - **本番環境では
instructorを使ってください。**PydanticとOpenAIの統合専用にメンテナンスされており、APIの変更を追跡してくれるため、自分で対応する必要がありません。 - スキーマの素早い確認には、JSON Formatter & Validatorが便利です。スキーマを貼り付けるだけで、セットアップ不要で不正な構造をひと目で発見できます。

