InfoWok
Intermediate

LLM Structured Outputs: Validated JSON in Python (2026)

A practical 2026 guide to structured outputs from LLMs in Python. You define the shape with Pydantic, enforce it with OpenAI Structured Outputs (responses.parse), go provider-agnostic with instructor, handle enums and nested models, and deal with the cases where output still fails — every snippet runnable.

SK
Sukhveer Kaur
Published July 4, 2026
4 min read
Title card 'LLM Structured Outputs' — a 2026 Python guide to getting validated JSON from LLMs with Pydantic and OpenAI Structured Outputs instead of fragile string parsingAI Engineering
LLM DATA · PYDANTIC
On this page +

Ask an LLM for JSON and it usually obliges — until the one time it wraps the JSON in prose, renames a field, or returns a number as a word. Then your json.loads throws, and a feature that demoed perfectly breaks in production. String parsing of model output is a trap.

Structured outputs fix this. Instead of hoping the model returns clean JSON, you hand it a schema and get back a validated object every time. This guide shows the modern Python approach end to end: define the shape with Pydantic, enforce it with OpenAI Structured Outputs, go provider-agnostic with instructor, and handle the cases that still fail. Every snippet runs on the current libraries.

🟡 Intermediate⏱️ 20 minStack: Python 3.10+, openai, pydantic, instructor
Before you start
🎯 Key takeaways
  • Structured outputs return validated data, not text you parse. You define a schema; the API guarantees the response matches it.
  • Pydantic is the schema layer. One BaseModel describes the shape and does the validation.
  • responses.parse is the OpenAI-native path; instructor is the provider-agnostic one.
  • Design still matters — enums, optional fields, and a retry on validation errors are what make it production-safe.

What structured outputs are (and why parsing fails)#

Structured outputs flip the problem. Normally you ask for JSON in the prompt and hope. With structured outputs, you attach a schema, and the model is constrained to produce output that fits it. The result is parsed and validated for you.

Prompt-and-hope fails because “usually valid” is not “always valid.” At scale, the model will eventually add a markdown fence, a trailing comment, or a field you did not ask for. A schema the API enforces removes an entire class of production bugs — the ones where the model was almost right.

Step 1: Define the shape with Pydantic#

Everything starts with the schema. A Pydantic model is the single source of truth for the fields and their types.

python
from pydantic import BaseModel
 
class Ticket(BaseModel):
    category: str
    priority: str
    summary: str

That is the whole contract. Pydantic both describes the shape and validates it, so you write the structure once and reuse it everywhere. New to it? The BaseModel primer covers the basics.

Step 2: Enforce it with OpenAI Structured Outputs#

Pass the model to responses.parse as text_format. The SDK sends the JSON Schema, and you get a parsed, validated object back on output_parsed.

python
from openai import OpenAI
from pydantic import BaseModel
 
class Ticket(BaseModel):
    category: str
    priority: str
    summary: str
 
client = OpenAI()  # reads OPENAI_API_KEY
 
response = client.responses.parse(
    model="gpt-5.4-mini",
    input="Classify this message: My invoice is wrong and I was charged twice!",
    text_format=Ticket,
)
 
ticket = response.output_parsed        # a validated Ticket instance
print(ticket.category, "|", ticket.priority)

No json.loads, no cleanup. response.output_parsed is a real Ticket, so the rest of your code works with a typed object, not a string.

💡 Tip

Prefer the Chat Completions API? The same pattern works with client.chat.completions.parse(...), passing your model as response_format and reading completion.choices[0].message.parsed.

Go provider-agnostic with instructor#

If you use more than one model provider, the instructor library gives you one API across all of them. It patches the client and returns your Pydantic model directly.

python
import instructor
from openai import OpenAI
from pydantic import BaseModel
 
class Ticket(BaseModel):
    category: str
    priority: str
    summary: str
 
client = instructor.from_openai(OpenAI())
 
ticket = client.chat.completions.create(
    model="gpt-5.4-mini",
    response_model=Ticket,
    messages=[{"role": "user", "content": "Classify: my package never arrived."}],
)
 
print(ticket.category)   # instructor returns the Pydantic object directly

Same response_model pattern, any provider — swap the client and model string and the rest is identical. instructor also adds automatic retries on validation errors, which we come back to below.

Handle the hard cases#

Real schemas are more than three strings. Structured outputs handle enums, optional fields, and nested models — and using them is how you stop the model from inventing values.

python
from enum import Enum
from pydantic import BaseModel
 
class Priority(str, Enum):
    low = "low"
    normal = "normal"
    high = "high"
 
class Item(BaseModel):
    name: str
    qty: int
 
class Order(BaseModel):
    order_id: str
    priority: Priority           # constrained to the enum values
    note: str | None = None      # optional field
    items: list[Item]            # nested list of models

An Enum constrains priority to three values, so the model cannot return “urgent-ish.” A nested list[Item] gives you structured line items, not a blob. Encode your rules in the schema and the model has to follow them — that is cheaper and more reliable than policing output in a prompt.

When outputs still fail#

Structured outputs remove most failures, not all. Two still happen. The model can refuse a request on safety grounds, in which case there is no parsed object — check for a refusal before using the result. And a value can miss a business rule your types do not capture, which raises a Pydantic ValidationError.

The fix for the second is a bounded retry: catch the error, feed it back, and ask the model to correct itself. instructor does this for you; if you roll your own, cap the attempts so a stubborn case cannot loop forever.

⚠️ Warning

Never trust a parsed object as correct just because it is valid. Structured outputs guarantee the shape, not the facts. For anything that drives money or actions, validate the values and add evals before you ship.

Common mistakes#

  • Parsing strings by hand. If you are running json.loads on model text, you are one odd response away from a crash. Use a schema.
  • JSON-shaped, not validated. A model returning JSON is not the same as validated data. Keep Pydantic in the loop.
  • Over-loose types. str everywhere lets the model invent values. Use enums and constraints to pin them down.
  • No retry path. Validation errors will happen. Catch them and retry with the error, with a hard cap on attempts.
  • Assuming valid means correct. The shape is guaranteed; the content is not. Check values that matter.

Summary#

Structured outputs turn model responses from text you hope to parse into data you can trust. Define the shape once with Pydantic, enforce it with responses.parse or go cross-provider with instructor, encode your rules with enums and nested models, and keep a retry path for the cases that slip through. That is the difference between a demo that parses today and a feature that holds up in production.

🧭 Where to go from here

Wiring this into your own app? Tell me the shape you’re trying to extract — a ticket, an invoice, a resume — and I’ll help you design the schema so the model can’t wander.

Frequently asked questions

What are structured outputs from an LLM? +
Structured outputs make the model return data that matches a schema you define — validated JSON you can use directly — instead of free-form prose you have to parse. In Python you describe the shape with a Pydantic model and the API guarantees the response conforms to it.
Do I still need Pydantic if the model returns JSON? +
Yes. The model returning JSON-shaped text is not the same as validated data. Pydantic defines the schema, validates types, and gives you a typed object. With OpenAI Structured Outputs it also generates the JSON Schema the API enforces, so the two work together.
What's the difference between response_format and tool calling? +
Use structured outputs (response_format / text_format) when you want the model's answer to the user shaped as data. Use tool calling when the model should decide to invoke a function. They can be combined, but they solve different problems.
How do I make this work with non-OpenAI models? +
Use the instructor library. It patches the client and returns Pydantic objects across providers — OpenAI, Anthropic, Google, and local models — so the same response_model pattern works everywhere.

References

  1. OpenAI — Structured model outputs (official guide)
  2. Pydantic — official docs
  3. instructor — structured outputs for LLMs
Written by
Sukhveer Kaur
Sukhveer KaurSoftware Developer & AI Engineer

Sukhveer is a software developer specialising in AI systems and backend engineering. She has hands-on experience designing agentic AI applications, working with large language model pipelines, autonomous agent frameworks, and cloud-native services in Java and Python. At InfoWok, she bridges the gap between cutting-edge AI research and practical implementation — helping developers understand and apply emerging technologies through clear, experience-backed writing.

New AI engineering guides, the day they ship

Real Python, production depth. No digest spam.

Comments