LLMs often return plain text, but applications usually need data in a fixed structure. For example, a weather app might need a city name, temperature, and weather condition from an LLM response.
Structured outputs let you define the expected format of an LLM response. With a Pydantic model, you can define the expected fields and validate the response as a Python object.
Let’s explore how to generate structured outputs using OpenAI and Claude.
1. Define the data structure
Before calling an LLM, define the data you expect it to return. Pydantic lets you describe this structure using a Python class.
from pydantic import BaseModel
class PersonData(BaseModel):
name: str
location: strHere, both fields must contain strings. Pydantic uses these types to validate the returned data and gives you a Python object that you can access using attributes such as `person.name`.
Both OpenAI and claude providers support two approaches:
- `parse()` — pass a Pydantic model and get a parsed object.
- Raw JSON schema — define the schema yourself and handle the response.
The `parse()` approach is usually simpler because you don't need to write the JSON schema or convert the response manually.
2. OpenAI: Responses API
OpenAI's Responses API provides `responses.parse()`, which accepts a Pydantic model and returns the parsed result.
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI()
class PersonData(BaseModel):
name: str
location: str
response = client.responses.parse(
model="gpt-4o",
instructions="Extract the person's name and location.",
input="My name is Alex and I live in Stockholm.",
text_format=PersonData,
)
person = response.output_parsed
print(person.name, person.location)The important parts are:
- `instructions` contains the instructions for the model.
- `text_format` specifies the Pydantic model that defines the expected output.
- `output_parsed` contains the parsed result as a Pydantic object.
For this example, the model should return the name Alex and the location Stockholm. You can then access these values directly without manually parsing a JSON string.
You can also use `responses.create()` API with a JSON schema. However, you need to define the schema and handle the response yourself, which requires more code.
3. Claude: Messages API
Claude follows a similar approach through its Messages API. You provide a Pydantic model, and the SDK helps return the response as a structured object.
from anthropic import Anthropic
from pydantic import BaseModel
client = Anthropic()
class PersonData(BaseModel):
name: str
location: str
response = client.messages.parse(
model="claude-haiku-4-5",
max_tokens=200,
system="Extract the person's name and location.",
messages=[
{
"role": "user",
"content": "My name is Alex and I live in Stockholm."
}
],
output_format=PersonData,
)
person = response.parsed_output
print(person.name, person.location)The main differences are:
- `system` provides the instructions.
- `output_format` specifies the Pydantic model.
- `parsed_output` contains the parsed result.
- `max_tokens` sets the maximum number of tokens Claude can generate and is required for this API call.
Notice that the Pydantic model is almost identical to the OpenAI example. The main changes are how you pass the input, specify the output format, and access the result.
Claude also supports raw JSON schemas through `messages.create()` and `output_config`, but `messages.parse()` is simpler when using Pydantic.
4. A real-world example: News classification
Imagine you are building a news application. You want an LLM to classify news articles as politics, sports, technology, or business.
Instead of getting a text explanation, you want the model to return a category and tell you whether the news is positive or negative.
First, define the expected output using Pydantic.
from typing import Literal
from pydantic import BaseModel
class NewsClassification(BaseModel):
category: Literal["politics", "sports", "technology", "business"]
sentiment: Literal["positive", "negative", "neutral"]The Literal types restrict the values the model can return. For example, the category must be one of the four listed options.
Now let's use this model with OpenAI and Claude.
response = client.responses.parse(
model="gpt-4o",
instructions="Classify the news category and sentiment.",
input="The company announced record profits this year.",
text_format=NewsClassification,
)
result = response.output_parsed
print(result.category, result.sentiment)5. Common mistakes and issues
Structured output makes LLM responses easier to work with, but errors can still happen. Here are four things to consider when building your application.
- 1
Handle API errors and parsing failures
A request may fail because of a network issue, rate limit, refusal, or incomplete response. The parsing helper may also raise an exception if the output cannot be parsed into your Pydantic model. Catch relevant exceptions, check for refusals or incomplete responses where supported, and retry only when appropriate.
- 2
Handle missing information and unexpected values
Use optional fields such as order_id: str | None when information may be missing. Instruct the model to return null rather than guess. Use Literal types when a field must have specific values, such as positive, negative, or neutral.
- 3
Handle incorrect classifications
A response may match your Pydantic model but still contain the wrong category. Improve accuracy by defining clear classification rules and adding few-shot examples to your system prompt. You can also use evals to measure accuracy and identify areas for improvement.
Takeaway
✓The short version
Use `parse()` with Pydantic when building AI applications with OpenAI or Claude. You get structured, validated Python objects with less code.
Once you understand this pattern, you can apply it to data extraction, classification, support assistants, and many other AI features.
Learn to build practical AI applications
Explore the course →