How to Use Pydantic with Instructor for Reliable Structured JSON Outputs
instructor, which basically turns Pydantic models into a schema that the LLM is forced to follow via tool-calling (function calling). The magic here is that instructor patches the OpenAI or Anthropic client, so instead of getting a raw string back, you get a fully validated Pydantic object. If the LLM hallucinates a field or misses a required key, Pydantic catches it immediately.
Here is the setup I'm using for a lead-scraping utility. I define exactly what I want, and if the LLM fails validation, instructor can actually feed the error back to the model to "self-correct" in a loop.
import instructor
from pydantic import BaseModel, Field, EmailStr
from openai import OpenAI
from typing import List, Optional
# This is the key: wrap the client
client = instructor.from_openai(OpenAI())
class UserProfile(BaseModel):
name: str
email: EmailStr # Built-in validation for email formats
company: str
role: str = Field(..., description="The job title, e.g., 'CTO' or 'Product Manager'")
tech_stack: List[str]
estimated_budget: Optional[int] = Field(None, description="Budget in USD")
# The call looks like a standard chat completion,
# but we pass 'response_model' to trigger the magic.
user = client.chat.completions.create(
model="gpt-4o",
response_model=UserProfile,
messages=[
{"role": "user", "content": "Extract info from: John Doe is the CTO of TechCorp, reachable at [email protected]. They use React and Python."}
],
)
print(user.name) # Output: John Doe
print(user.tech_stack) # Output: ['React', 'Python']A few productivity gains I've noticed since moving to this workflow:
Strong Typing for Downstream Logic
Because user is a Pydantic object, my IDE (Cursor) gives me full autocomplete. I no longer have to remember if the key was user_email or email in some random JSON blob.
Validation as a Prompt
The Field(description="...") isn't just for documentation; instructor injects these descriptions into the tool definition sent to the LLM. If the model is consistently getting a field wrong, I don't change the prompt—I just sharpen the Pydantic description.
Handling Retries
One "gotcha" is that LLMs still occasionally trip up on complex nested lists. I use the max_retries parameter in the .create() call. If Pydantic throws a ValidationError, instructor automatically sends that error back to the LLM and asks it to fix the JSON.
Config Tips for Production
Use EmailStr and HttpUrl: Don't just use str. Pydantic's specialized types act as an automated QA layer.
Keep models flat: The deeper the nesting of Pydantic models, the more likely the LLM is to lose track of the schema. If you need a complex structure, try to flatten it or break it into multiple sequential calls.
Prefer GPT-4o or Claude 3.5 Sonnet: While smaller models support tool-calling, they often struggle with strict type constraints (like returning an integer instead of a stringified number).
This setup has effectively eliminated the "JSON parsing error" from my logs. Instead of writing 20 lines of try-except blocks around json.loads(), I let the schema handle the heavy lifting.
All Replies (0)
No replies yet — be the first!
