How to Use Pydantic with Instructor for Reliable Structured JSON Outputs

PromptWizard Advanced 4/27/2026 271 views 4 likes 2 min read

Getting clean, structured JSON from an LLM is usually a nightmare of "Please output ONLY JSON" prompts and fragile regex parsing. I finally stopped fighting the prompt and switched to instructor, which basically turns Pydantic models into a schema that the LLM is forced to follow via tool-calling (function calling).

How to Use Pydantic with Instructor for Reliable Structured JSON Outputs

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.

Related examples in this direction are worth a look in these real-world AI monetization case studies, with plenty of directly applicable cases.

All Replies (0)

No replies yet — be the first!

Write a Reply

Markdown supported