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/$refin nested models β OpenAI doesn't resolve internal referencesanyOffromOptionalfields β some OpenAI models reject this pattern- Missing
additionalProperties: falseβ required by strict mode - Extra Pydantic metadata fields like
titleon 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.

