Fix Pydantic ValidationError: Invalid JSON for OpenAI Function Call

intermediate🧠 AI Tools2026-05-17| Python 3.9+, pydantic>=2.0, openai>=1.0.0 (also affects pydantic v1 with openai<1.0)

Error Message

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

What Triggers This Error

You define a Pydantic model, convert it to a JSON schema, pass it to OpenAI's function calling API β€” and get back:

ValidationError: Invalid JSON for OpenAI function call

Two things can go wrong here. OpenAI's API might reject the schema outright. Or it accepts the call fine, but parsing the response back into a Pydantic object fails because the structure doesn't match what you expected. Either way, the culprit is almost always a mismatch between what Pydantic generates and what OpenAI actually expects.

Debug Process

Step 1 β€” Print the actual schema you're sending

Don't guess β€” dump the schema and read it.

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))

Paste the output into JSON Formatter & Validator β€” it highlights structural problems instantly and is fully browser-side, nothing leaves your machine.

Step 2 β€” Check what OpenAI actually received

Turn on debug logging to capture the raw API request:

import openai
import logging

logging.basicConfig(level=logging.DEBUG)
openai.log = "debug"

Find the functions or tools payload in the debug output. That's the exact schema OpenAI is validating against.

Step 3 β€” Identify the root cause

The most common culprits:

  • Using .schema() instead of .model_json_schema() (Pydantic v2 breaking change)
  • $defs / $ref in nested models β€” OpenAI doesn't resolve internal references
  • anyOf from Optional fields β€” some OpenAI models reject this pattern
  • Missing additionalProperties: false β€” required by strict mode
  • Extra Pydantic metadata fields like title on nested properties

Solutions

Fix 1 β€” Use the correct schema method for your Pydantic version

Pydantic v2 renamed .schema() to .model_json_schema(). This trips up a lot of migrations β€” the old method still works in v2 but returns a deprecation warning and sometimes an incompatible structure.

# Pydantic v1
schema = MyModel.schema()

# Pydantic v2 β€” use this
schema = MyModel.model_json_schema()

Fix 2 β€” Flatten nested model references

Nest a Pydantic model inside another, and the generated schema sprouts $defs and $ref pointers. OpenAI's function calling API doesn't resolve these internally β€” it sees an unresolved reference and fails.

from pydantic import BaseModel
from typing import List

class Item(BaseModel):
    name: str
    quantity: int

class Order(BaseModel):
    items: List[Item]
    shipping_address: str

# This will contain $defs β€” problematic for OpenAI
raw_schema = Order.model_json_schema()

# Flatten $defs by inlining them
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))

Fix 3 β€” Handle Optional fields that generate anyOf

Pydantic v2 renders Optional[str] as {"anyOf": [{"type": "string"}, {"type": "null"}]}. OpenAI strict mode rejects null types inside anyOf. Two approaches work β€” override per field, or strip nulls from the whole schema in one pass.

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"}  # Override anyOf with plain type
    )

Or post-process the schema to remove null from anyOf:

def strip_null_anyof(schema: dict) -> dict:
    """Replace {anyOf: [T, null]} with just 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()}

Fix 4 β€” Add additionalProperties: false for strict mode

Strict function calling has one hard rule: every object in the schema must explicitly disallow extra properties. Pydantic doesn't add this by default.

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())

Fix 5 β€” Use instructor to skip the manual work

The instructor library was built specifically for this problem. It handles $ref flattening, anyOf cleanup, and strict mode flags without you writing a single schema utility function:

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)

No schema tweaking. No post-processing helpers. Worth adopting if you hit this more than once.

Verify the Fix

Before hitting the real API, test your schema locally with jsonschema:

import jsonschema
import json

schema = flat_schema  # your processed schema

# Check schema is valid JSON Schema
try:
    jsonschema.Draft7Validator.check_schema(schema)
    print("Schema is valid")
except jsonschema.SchemaError as e:
    print(f"Schema error: {e.message}")

# Test against a sample payload
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}")

Both checks pass? Make the API call. No ValidationError means the fix landed.

Lessons Learned

  • Print the schema first. Paste it into a validator and read it. Most errors become obvious immediately β€” no need to touch the API at all.
  • Pydantic v1 β†’ v2 migration is a common trap. If this error appeared right after upgrading, .schema() β†’ .model_json_schema() is probably the only fix you need.
  • OpenAI strict mode means strict. Every field declared, every object with additionalProperties: false, no $refs anywhere in the tree.
  • For production, use instructor. It's maintained specifically for Pydantic-to-OpenAI integration and tracks API changes so you don't have to.
  • For quick schema inspection, JSON Formatter & Validator is handy β€” paste the schema and spot malformed structures at a glance, no setup needed.

Related Error Notes