# 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.

*Source: https://www.infowok.com/llm-structured-outputs-python-2026/ · Sukhveer Kaur · Published July 4, 2026*

---

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.

<Prerequisites>

- Comfortable with Python and basic classes
- New to Pydantic? Read the [BaseModel primer](/pydantic-basemodel-primer/) and the [type hints primer](/python-type-hints-primer/)
- An OpenAI API key set in your environment — see the [API key primer](/llm-api-key-setup-primer/)

</Prerequisites>

<KeyTakeaways>

- **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.

</KeyTakeaways>

## 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](/pydantic-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.**

<Callout type="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`.

</Callout>

## Go provider-agnostic with instructor

If you use more than one model provider, the [instructor](https://python.useinstructor.com/) 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.

<Callout type="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](/ai-agent-evaluation-metrics-frameworks-2026/) before you ship.

</Callout>

## 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.

<NextSteps>

- **Building agents?** The [OpenAI Agents SDK tutorial](/openai-agents-sdk-tutorial-python/) uses the same idea via its `output_type`.
- **Foundations?** Read the [Pydantic BaseModel primer](/pydantic-basemodel-primer/).
- **Shipping to prod?** Add [evals](/ai-agent-evaluation-metrics-frameworks-2026/) so a schema or value regression fails a test.

</NextSteps>

*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.*
