Text size
Dark mode
RecallRunGuides & benchmarks
Learn to build real systemsGuides•Benchmarks•Experiments•Community
Search the site/Free checklist

Guides/OpenAI SDK

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
  1. 1. responses.parse with a Pydantic model
  2. 2. What the SDK sends
  3. 3. Refusals and incomplete output
  4. 4. Nested and list outputs
  5. 5. Where structured outputs shine
  6. Recap

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
Valid shape is not the same as correct content

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) returns output_parsed as a Pydantic instance.
  • Strict JSON Schema constrains generation: required fields, enums, no extras.
  • Check status and 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.

One email to confirm. Unsubscribe any time. What's in it · Privacy

Want the production version of this? See ShipRAG: A production RAG + MCP starter kit for FastAPI: hybrid search, validated citations, evals in CI, Docker, 32 tests.

Written by

Alby Tomy

Python/FastAPI Engineer | Data Engineer | AI Engineer building RAG and MCP systems.

X · GitHub · Contact & enquiries