Structured output with response_format
response_format is a create_agent option that asks the agent for an answer shaped like a Pydantic class and returns it as structured_response, so your code reads fields instead of parsing a sentence.
Last updated: 27 Sep, 2026 · LangChain 1.4
Structured output from a model, without an agent
Structured output means asking a model for a response in a format that matches a schema you give it, so your code can read it and use it in the next step. The video starts with Pydantic, because it offers field validation, descriptions and nested structures. A Movie class inherits BaseModel and declares four fields: the title (a string), the year the movie was released (an int), the director (a string) and the rating out of 10 (a float). Each Field carries a description, which tells the model which value goes in which field, and Pydantic checks each value against its type. with_structured_output(Movie) wraps the model so that its reply is a Movie object. Asked without the wrapper, "Provide details about the movie Inception" gets paragraphs about the film; asked through the wrapper, the same question gets four typed fields. There are no tools and no loop here, only one call. Here is the movie example from the video, run on Groq:
from langchain.chat_models import init_chat_model
model = init_chat_model("groq:openai/gpt-oss-120b")
from pydantic import BaseModel, Field
class Movie(BaseModel):
title: str = Field(description="The title of the movie")
year: int = Field(description="This year the movie was released")
director: str = Field(description="The director of the movie")
rating: float = Field(description="The movies rating out of 10")
model_with_structure = model.with_structured_output(Movie)
response = model_with_structure.invoke("Provide details about the movie Inception")
print(repr(response))Movie(title='Inception', year=2010, director='Christopher Nolan', rating=8.8)
Pydantic also supports nested structures. An Actor has a name and a role, both strings. A movie has many actors, so MovieDetails holds a cast that is a list of Actor objects, a list of genres, and a budget that defaults to None, described as "Budget in millions USD". The same Inception question now returns the cast as a list of actors and the genres as a list of strings. For a nested schema on Groq, pass method="json_schema": the default method asks the model for a tool call, and with this schema it sometimes left the required lists out.
from langchain.chat_models import init_chat_model
model = init_chat_model("groq:openai/gpt-oss-120b")
from pydantic import BaseModel, Field
class Actor(BaseModel):
name: str
role: str
class MovieDetails(BaseModel):
title: str
year: int
cast: list[Actor]
genres: list[str]
budget: float | None = Field(None, description="Budget in millions USD")
model_with_structure = model.with_structured_output(MovieDetails, method="json_schema")
response = model_with_structure.invoke("Provide details about the movie Inception")
print(repr(response))MovieDetails(title='Inception', year=2010, cast=[Actor(name='Leonardo DiCaprio', role='Dom Cobb'), Actor(name='Joseph Gordon-Levitt', role='Arthur'), Actor(name='Elliot Page', role='Ariadne'), Actor(name='Tom Hardy', role='Eames'), Actor(name='Ken Watanabe', role='Saito'), Actor(name='Marion Cotillard', role='Mal'), Actor(name='Michael Caine', role='Professor Miles')], genres=['Science Fiction', 'Action', 'Thriller'], budget=160.0)
budget=160.0 means 160 million: the field description set the unit, and the model followed it. In the video the model lists Tom Hardy as Bane; his role in Inception is Eames, as here, which is a reminder that a model's facts still need checking.
An agent can do the same after it has used its tools, through the response_format option of create_agent.
Pulling contact details out of text
The video names the third kind of schema, data classes, and then moves from the model to an agent. A data class, part of Python since 3.7, is a class that mainly holds data, made with the @dataclass decorator and with no input validation. The first example still uses Pydantic: a ContactInfo class with a name, an email and a phone, all strings. Instead of with_structured_output, the class goes to create_agent as response_format, so every answer of this agent comes back in that shape. The agent gets one message, "Extract contact info from: John Doe, john@example.com, (555) 123-4567", and returns the object in structured_response.
from pydantic import BaseModel, Field
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy
class ContactInfo(BaseModel):
"""Contact information for a person."""
name: str = Field(description="The name of the person")
email: str = Field(description="The email address of the person")
phone: str = Field(description="The phone number of the person")
agent = create_agent(
model="groq:openai/gpt-oss-120b",
response_format=ToolStrategy(ContactInfo) # Groq needs ToolStrategy, explained below
)
result = agent.invoke({
"messages": [{"role": "user", "content": "Extract contact info from: John Doe, john@example.com, (555) 123-4567"}]
})
print(repr(result["structured_response"]))ContactInfo(name='John Doe', email='john@example.com', phone='(555) 123-4567')
The full result holds the messages, the human message and the AI reply, and next to them structured_response. The model read "John Doe, john@example.com, (555) 123-4567" and filled each field, and Pydantic checked every value against its type. The field descriptions in the class guide the model, the same way a tool's docstring does.
The same schema three ways
The video then writes the same schema as a TypedDict and as a data class and passes each one to response_format. They are different ways to describe one shape, and any of them works; what differs is what you get back.
from pydantic import BaseModel, Field
class ContactInfo(BaseModel):
"""Contact information for a person."""
name: str = Field(description="The name of the person")
email: str = Field(description="The email address of the person")
phone: str = Field(description="The phone number of the person")
# structured_response:
# ContactInfo(name='John Doe', email='john@example.com', phone='(555) 123-4567')- Pydantic validates the values and gives an object with attributes. Use it when a wrong value should be caught.
- TypedDict gives a plain dictionary with no validation. Use it when you only need the shape.
- dataclass gives an object with attributes, without Pydantic's checks.
In the TypedDict and dataclass versions the comments are notes for you; the model never sees them. To give the model a description there, use Annotated[str, ..., "The name of the person"].
Now the shop. Its ticket system wants two fields for every question, the order id and its status, not a sentence to pick apart. A Pydantic class describes that shape, and this time the agent also has a tool to call.
The response_format option
agent = create_agent(model, tools=[...], response_format=Ticket) # Ticket is a Pydantic class
result = agent.invoke(inputs)
result["structured_response"] # a Ticket instance, ready for codeDefining the Ticket class
Define the shape as a Pydantic class with the fields your code wants.
from pydantic import BaseModel
class Ticket(BaseModel):
"""A support ticket for one order."""
order_id: str
status: strGiven a schema, create_agent picks a strategy. If the model supports structured output natively, as OpenAI's and Anthropic's do, it asks the provider for it; that is what OpenAI's gpt-5 does in the video. Groq's gpt-oss-120b needs ToolStrategy, which is why the contact example above passes it. Otherwise it uses ToolStrategy: it adds one more tool, named after the class, and requires the model to call a tool on every turn. The arguments of that final call are the answer.
Pick one to watch it run, step by step.
Wiring in response_format
This lesson's agent answers order questions with lookup_order, the tool built in Tools: a function the model can call. Start the file with it, then the Ticket class from above.
from langchain.tools import tool
ORDERS = {"A17": "shipped on 3 March", "C40": "waiting for stock"}
@tool
def lookup_order(order_id: str) -> str:
"""Look up an order's shipping status by its id, such as A17."""
status = ORDERS.get(order_id)
return f"{order_id} {status}." if status else f"{order_id} is not an order we have."Wire the class in with response_format. Groq's gpt-oss-120b cannot return JSON and call tools in the same request, so wrap the class in ToolStrategy: it turns Ticket into one more tool, and the model finishes by calling it.
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy
from langchain.chat_models import init_chat_model
model = init_chat_model("groq:openai/gpt-oss-120b", temperature=0) # uses your GROQ_API_KEY
agent = create_agent(model, tools=[lookup_order], response_format=ToolStrategy(Ticket),
system_prompt="You are the support assistant for a small online shop. Look up the order, then fill in the Ticket using only what the tools returned.")The structured answer on one question
Add these lines to the end of agent.py: they ask one question and read both the structured response and the messages behind it.
result = agent.invoke({"messages": [{"role": "user", "content": "Where is my order A17?"}]})
print(repr(result["structured_response"]))
for message in result["messages"]:
print(f"{message.type:<6} {message.text or message.tool_calls[0]['name']}")Ticket(order_id='A17', status='shipped') human Where is my order A17? ai lookup_order tool A17 shipped on 3 March. ai Ticket tool Returning structured response: order_id='A17' status='shipped'
What structured_response held
structured_responseis aTicketobject, ready for code to use.- The conversation shows how it got there: the lookup, then a call to the
Tickettool, then a tool message that ends the loop by returning the ticket.
A sentence vs response_format
| Plain answer | response_format | |
|---|---|---|
| What you get back | Text in result["messages"] | A Ticket in result["structured_response"] |
| Your code then | Parses the sentence | Reads fields |
| If a field is missing | You notice late | The class can require it |
When to use structured output
- Feeding an answer into another system: a ticket, a row in a database, a form.
- Any time code, not a person, reads what the agent produced.
ToolStrategy the class name becomes a tool name, so the model must call it to finish. If your prompt or model never calls that tool, the run has no structured_response to return.Related
- Previous: Runtime context: who is asking
- Next: Validation errors, and the retry
- Reference: Structured output
- Add a field
customer: str = "unknown"toTicketand print the response again. - Ask about B22 and read the status the ticket gets.
- Print
result["structured_response"].model_dump()to get a plain dictionary.
Little by little, you're building something great.