Pydantic ValidationError: OpenAIファンクションコールの無効なJSONを修正する

intermediate🧠 AI Tools2026-05-17| Python 3.9+、pydantic>=2.0、openai>=1.0.0(pydantic v1とopenai<1.0でも発生)

Error Message

ValidationError: Invalid JSON for OpenAI function call
#pydantic#openai#function-calling#json-schema

このエラーが発生する原因

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が便利です。スキーマを貼り付けるだけで、セットアップ不要で不正な構造をひと目で発見できます。

Related Error Notes