What used to require multiple validation steps now happens automatically. The payoff is cleaner code, fewer bugs, and applications that are significantly easier to maintain.
The Problem with Manual JSON Parsing
Before this integration, extracting structured data from LLMs meant dealing with raw JSON strings. Even with OpenAI’s Structured Outputs (which guarantees schema adherence), developers still received plain JSON that needed manual handling.
The workflow looked like this:
- Model returns a JSON string conforming to your schema
- You manually parse it with json.loads()
- You cast fields to the right types (is age a string or integer?)
- You validate constraints yourself (is rating between 1 and 5?)
- You handle missing keys that could cause KeyError exceptions
Each step is an opportunity for things to break in production.
How Pydantic Fixes This
Pydantic is a Python library that validates data through type annotations. You define a class with typed fields, and Pydantic enforces those types automatically. When data arrives that doesn’t match your schema, you get an immediate, descriptive error rather than silent corruption.
Here’s a simple example:
from pydantic import BaseModel
class PersonInfo(BaseModel):
name: str
age: int
city: str
That’s it. Pydantic will ensure name and city are strings, and age is an integer. Pass in bad data and it fails fast.
The Integration with OpenAI
OpenAI’s newer SDK lets you pass a Pydantic model directly as your response format. The SDK handles schema generation in the background, and crucially, returns a proper Python object instead of a dictionary.
Before (manual parsing):
from openai import OpenAI
import json
client = OpenAI(api_key="your_api_key")
tools = [
{
"type": "function",
"function": {
"name": "extract_person_info",
"strict": True,
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"}
},
"required": ["name", "age", "city"],
"additionalProperties": False
}
}
}
]
response = client.chat.completions.create(
model="gpt-4o-mini",
tools=tools,
tool_choice={"type": "function", "function": {"name": "extract_person_info"}},
messages=[
{"role": "user", "content": "Extract info from: 'Maria is 32 years old and lives in Athens.'"}
]
)
result = json.loads(response.choices[0].message.tool_calls[0].function.arguments)
print(result["name"]) # "Maria" but what if the key is missing? KeyError.
print(result["age"]) # might be "32" as a string, not 32 as an integer
After (with Pydantic):
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI(api_key="your_api_key")
class PersonInfo(BaseModel):
name: str
age: int
city: str
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{"role": "user", "content": "Extract info from: 'Maria is 32 years old and lives in Athens.'"}
],
response_format=PersonInfo
)
result = response.choices[0].message.parsed
print(result.name) # "Maria" - always a string
print(result.age) # 32 - always an integer, never a string
print(result.city) # "Athens" - always a string
The difference is striking. No manual parsing, no type guessing, no KeyError surprises.
Handling Complex, Nested Data
Pydantic shines when dealing with nested structures. Define complex hierarchies as Python classes, and you get clean dot notation access to nested fields.
from pydantic import BaseModel
from typing import List
class Address(BaseModel):
street: str
city: str
country: str
class ContactInfo(BaseModel):
name: str
email: str
address: Address
phone_numbers: List[str]
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{
"role": "user",
"content": "Extract contact info from: 'Maria Mouschoutzi, [email protected], Ermou 15, Athens, Greece. Phone: +30 210 1234567, +30 697 8901234'"
}
],
response_format=ContactInfo
)
contact = response.choices[0].message.parsed
print(contact.name) # "Maria Mouschoutzi"
print(contact.address.city) # "Athens"
print(contact.phone_numbers[0]) # "+30 210 1234567"
No KeyError exceptions. No traversing nested dictionaries. Just clean, Pythonic access to your data.
Adding Validation Rules
Pydantic’s Field class lets you add granular constraints. These descriptions also serve as instructions to the LLM, guiding it on expected content.
from pydantic import BaseModel, Field
from typing import Optional
class ProductReview(BaseModel):
product_name: str = Field(description="The name of the product being reviewed")
rating: int = Field(ge=1, le=5, description="Rating from 1 to 5 stars")
sentiment: str = Field(description="One of: positive, neutral, negative")
summary: str = Field(max_length=200, description="A brief summary of the review")
verified_purchase: Optional[bool] = Field(default=None, description="Whether this is a verified purchase")
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{
"role": "user",
"content": "Extract a structured review from: 'Absolutely love this coffee machine! Makes perfect espresso every time. 5 stars, would recommend to anyone.'"
}
],
response_format=ProductReview
)
review = response.choices[0].message.parsed
print(review.product_name) # "coffee machine"
print(review.rating) # 5 (guaranteed to be between 1 and 5)
print(review.sentiment) # "positive"
print(review.summary) # "Makes perfect espresso every time."
Handling Model Refusals Gracefully
When an LLM refuses a request (due to policy violations or other constraints), the parsed field returns None and the refusal field contains the reason. This provides a clean, structured way to handle denials.
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Extract info from the job posting above."}],
response_format=JobPosting
)
message = response.choices[0].message
if message.refusal:
print(f"Model refused: {message.refusal}")
else:
job = message.parsed
print(f"Extracted: {job.job_title} at {job.company_name}")
Real-World Example: Job Posting Extraction
Let’s build a practical application that extracts structured data from a job posting:
from pydantic import BaseModel, Field
from typing import List, Optional
from openai import OpenAI
client = OpenAI(api_key="your_api_key")
class SalaryRange(BaseModel):
min_salary: Optional[int] = Field(default=None, description="Minimum salary in USD per year")
max_salary: Optional[int] = Field(default=None, description="Maximum salary in USD per year")
currency: str = Field(default="USD", description="Currency code")
class JobPosting(BaseModel):
job_title: str = Field(description="The job title or role name")
company_name: str = Field(description="The name of the hiring company")
location: str = Field(description="Job location, e.g. 'Athens, Greece' or 'Remote'")
employment_type: str = Field(description="One of: full-time, part-time, contract, freelance")
required_skills: List[str] = Field(description="List of required technical skills")
years_of_experience: Optional[int] = Field(default=None, description="Minimum years of experience required")
salary: Optional[SalaryRange] = Field(default=None, description="Salary range if mentioned")
remote_friendly: bool = Field(description="Whether remote work is allowed")
job_post_text = """
Senior Python Engineer at pialgorithms (Athens, Greece / Remote)
We are looking for an experienced Python developer to join our AI team. You will work on our document management platform, building intelligent search and extraction features using LLMs and RAG pipelines.
Requirements:
- 5+ years of Python experience
- Strong knowledge of FastAPI, LangChain, and vector databases
- Experience with OpenAI API and Pydantic
- Familiarity with Docker and cloud deployment (AWS/GCP)
Salary: EUR 60,000 - EUR 85,000 per year
Full-time position. Remote-friendly.
"""
response = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "You are an expert at extracting structured information from job postings."
},
{
"role": "user",
"content": f"Extract all relevant information from this job posting:\n\n{job_post_text}"
}
],
response_format=JobPosting
)
job = response.choices[0].message.parsed
print(f"Title: {job.job_title}")
print(f"Company: {job.company_name}")
print(f"Location: {job.location}")
print(f"Remote: {job.remote_friendly}")
print(f"Skills: {', '.join(job.required_skills)}")
print(f"Experience: {job.years_of_experience} years")
if job.salary:
print(f"Salary: {job.salary.min_salary} - {job.salary.max_salary} {job.salary.currency}")
Output:
Title: Senior Python Engineer
Company: pialgorithms
Location: Athens, Greece / Remote
Remote: True
Skills: Python, FastAPI, LangChain, vector databases, OpenAI API, Pydantic, Docker, AWS, GCP
Experience: 5 years
Salary: 60000 - 85000 EUR
Why This Matters
This integration addresses what has traditionally been unpredictable about AI applications. Instead of writing defensive code that anticipates every possible malformed response, you define your schema once and trust Pydantic to enforce it. The benefits compound in production:
- Type safety: Fields are guaranteed to be the right type
- Early error detection: Invalid data fails immediately with clear error messages
- Reduced boilerplate: No manual parsing, type casting, or validation logic
- Better IDE support: Your IDE understands the structure and can autocomplete field names
- Cleaner code: Less defensive programming means more readable applications
The combination of Pydantic and OpenAI’s Structured Outputs represents a significant step forward in making LLM-powered applications reliable and maintainable at scale.
Follow Hashlytics on Bluesky, LinkedIn , Telegram and X to Get Instant Updates



