OpenAI SDK: Structured outputs with Pydantic
Most application code does not want prose. It wants data : a category, an extracted invoice, a list of action items.
In this post
What this post covers
Most application code does not want prose. It wants data: a category, an extracted invoice, a
list of action items. Structured outputs make the model’s reply conform to a
JSON Schema you supply. With the SDK’s responses.parse, you pass a
Pydantic model and get back an instance of it, typed and validated, with no regular
expressions and no “please answer in JSON” pleading.
1. responses.parse with a Pydantic model
from typing import Literal
from pydantic import BaseModel, Field
from fake_openai import fake_client
class Complaint(BaseModel):
order_id: str
category: Literal["delivery", "damaged", "refund", "other"]
sentiment: Literal["positive", "neutral", "negative"]
wants_callback: bool
summary: str = Field(description="One sentence, under 20 words")
email = ("Hi, order ORD-7731 arrived two weeks late and the lamp was cracked. "
"Please call me back, this is really frustrating.")
client = fake_client(script=['{"order_id": "ORD-7731", "category": "damaged", "sentiment": "negative", '
'"wants_callback": true, "summary": "Late delivery and cracked lamp; customer wants a call."}'])
response = client.responses.parse(
model="gpt-5.6-luna",
instructions="Extract the complaint details from the customer's email.",
input=email,
text_format=Complaint,
)
c = response.output_parsed # a Complaint instance, not a string
print(type(c).__name__, c.order_id, c.category, c.wants_callback)
print(c.model_dump())
Output
Complaint ORD-7731 damaged True
{'order_id': 'ORD-7731', 'category': 'damaged', 'sentiment': 'negative', 'wants_callback': True, 'summary': 'Late delivery and cracked lamp; customer wants a call.'}
2. What the SDK sends
the Pydantic model becomes a strict JSON Schema
import json
from typing import Literal
from pydantic import BaseModel
from fake_openai import fake_client
class Label(BaseModel):
category: Literal["delivery", "damaged", "refund", "other"]
confidence: float
client = fake_client(script=['{"category": "refund", "confidence": 0.9}'])
client.responses.parse(model="gpt-5.6-luna", input="I want my money back", text_format=Label)
print(json.dumps(client.fake.requests[0]["body"]["text"], indent=2))
Output
{
"format": {
"type": "json_schema",
"strict": true,
"name": "Label",
"schema": {
"properties": {
"category": {
"enum": [
"delivery",
"damaged",
"refund",
"other"
],
"title": "Category",
"type": "string"
},
"confidence": {
"title": "Confidence",
"type": "number"
}
},
"required": [
"category",
"confidence"
],
"title": "Label",
"type": "object",
"additionalProperties": false
}
}
}
"strict": true means the model’s output is constrained to the schema during
generation: every required field present, enums respected, no extra keys. That is stronger than
asking nicely. Strict mode supports a large subset of JSON Schema, so every field must be required
(model optional values as str | None) and additionalProperties is
false. The SDK converts your Pydantic model into that form for you.
3. Refusals and incomplete output
response = client.responses.parse(model=MODEL, input=text, text_format=Complaint)
if response.status == "incomplete": # e.g. hit max_output_tokens
raise RuntimeError(f"incomplete: {response.incomplete_details}")
for item in response.output: # safety refusals come back as content parts
for part in getattr(item, "content", []):
if part.type == "refusal":
handle_refusal(part.refusal) # don't try to parse it
complaint = response.output_parsed # None if there was no parsable output
4. Nested and list outputs
from pydantic import BaseModel
from fake_openai import fake_client
class LineItem(BaseModel):
product: str
quantity: int
unit_price: float
class Invoice(BaseModel):
invoice_no: str
customer: str
items: list[LineItem]
total: float
scripted = ('{"invoice_no": "INV-204", "customer": "Asha Nair", "items": ['
'{"product": "Desk Lamp", "quantity": 2, "unit_price": 1499.0}, '
'{"product": "Notebook", "quantity": 5, "unit_price": 120.0}], "total": 3598.0}')
client = fake_client(script=[scripted])
inv = client.responses.parse(model="gpt-5.6-luna", input="(invoice text…)", text_format=Invoice).output_parsed
computed = sum(i.quantity * i.unit_price for i in inv.items)
print(inv.invoice_no, len(inv.items), "items; model total", inv.total, "| recomputed", computed)
print("totals agree:", abs(computed - inv.total) < 0.01)
Output
INV-204 2 items; model total 3598.0 | recomputed 3598.0
totals agree: True
The schema guarantees a well-formed object, not true values. The model can still misread a number.
Validate business rules in code: recompute totals, check that ids exist in your database, and range-check
dates. Pydantic validators (@field_validator) are a good place for this.
5. Where structured outputs shine
Extraction
Emails, invoices, CVs, support tickets → records you can insert into a database.
Classification & routing
Enum fields: category, priority, language, which team or model should handle it.
UI generation
A fixed shape your front end renders: a title, bullet points, a chart spec.
Recap
responses.parse(text_format=Model)returnsoutput_parsedas a Pydantic instance.- Strict JSON Schema constrains generation: required fields, enums, no extras.
- Check
statusand refusals before trusting the parsed object. - Schema-valid ≠ correct: validate business rules in code.
Get the free RAG production checklist
25 checks to run before real users see your RAG feature: exact-match misses, invented citations, prompt injection through documents, quality that slips after a prompt change. Confirm your email and the PDF comes with the welcome email, plus each new experiment.