The model emits prose. Your program needs data.
You ask a model to extract some fields. It replies:
Sure! Here's the information I extracted from the text you provided:
{"name": "Maya", "age": 18}
Hope that helps!
json.loads() on that raises JSONDecodeError immediately. So you write a regex to grab the part between the braces. Then you discover it sometimes wraps the JSON in a markdown code fence. Sometimes it uses single quotes. Sometimes there's a trailing comma. Sometimes it explains the JSON in a fourth line that also contains braces.
This is the most tedious and most frequent problem in LLM engineering. Output parsers exist to end it.
Declare what you want with Pydantic
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
class Ticket(BaseModel):
category: str = Field(description="One of: support, billing, technical, other")
urgency: int = Field(description="1-5, where 5 is most urgent")
summary: str = Field(description="One-line summary, under 15 words")
parser = PydanticOutputParser(pydantic_object=Ticket)
print(parser.get_format_instructions())
Run that — no API key required — and you'll see a block of text that starts like this:
The output should be formatted as a JSON instance that conforms to the JSON schema below.
...
{"properties": {"category": {"description": "One of: support, billing, technical, other", "type": "string"}, ...}}
That's the whole trick. Your class definition gets translated into instructions for the model. You didn't write that paragraph; the parser generated it from your fields and their description text.
Now wire it up:
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "You analyze support tickets. {format_instructions}"),
("user", "{text}"),
]).partial(format_instructions=parser.get_format_instructions())
chain = prompt | llm | parser
result = chain.invoke({"text": "Ordered three days ago, still not shipped, support won't reply. I want a refund."})
print(result.category, result.urgency) # support 4
print(type(result)) # <class '__main__.Ticket'>
result is a real Python object. It has type hints, your editor autocompletes its fields, and a typo in a field name fails loudly instead of returning None.
.partial() means "fill this variable now and leave the rest for later." The format instructions never change between calls, so there's no reason to pass them every time.
You maintain one definition — the class — and the prompt text and the validation both follow from it. Rename a field and both update themselves. That's the benefit a regex can never give you.
Lighter options
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
StrOutputParser() # plain text — the one you'll use most
JsonOutputParser() # a dict, with no field validation
Pick by what you need: structure that must be correct → Pydantic. Just text → Str. Rough exploration → Json.
JsonOutputParser is genuinely useful while you're still figuring out what fields you want. It becomes a liability in production, because a missing field slips through as a missing dict key and blows up three functions downstream, far from the cause.
The more reliable route: structured output
Most major providers now support structured output / function calling, where the provider constrains decoding so the response is guaranteed to be valid JSON matching your schema. That's fundamentally stronger than asking a model to please follow instructions. Use it when it's available:
structured_llm = llm.with_structured_output(Ticket)
result = structured_llm.invoke("Ordered three days ago, still not shipped.")
print(result.category) # support
One line, and no format instructions in the prompt at all.
The catch: this depends on provider support. Small local models and older models may not have it, and the method name has moved around across LangChain versions — check the current docs if the import or the method isn't there. When it isn't available, fall back to PydanticOutputParser, which works with any model that can produce text.
When parsing fails anyway
Even the reliable route has a failure rate. Production code has to catch it:
from langchain_core.exceptions import OutputParserException
try:
result = chain.invoke({"text": text})
except OutputParserException as e:
print("Parse failed. Raw output:", e)
result = Ticket(category="other", urgency=1, summary="parse failed")
print(result.category)
When the model returns something unparseable, you get the printed raw output plus a usable Ticket object, and the request continues instead of returning a 500.
Three rules that follow:
- Set
temperature=0. Structured extraction has one correct answer. Randomness only costs you. - Retry once, with the error text included. Feed the model its own broken output and the parser's complaint. It very often gets it right the second time. Retry once, not forever — a model that fails twice usually fails ten times, and you're paying for each attempt.
- Always have a fallback value. A single parse failure should degrade one field, not take down the request.
And do log the raw output when parsing fails. A swallowed exception with no log is the worst possible outcome: your users see nonsense and you have nothing to debug with.
Testing this without an API key
Notice that parser is an ordinary object. You can test your parsing logic entirely offline:
raw = '{"category": "billing", "urgency": 2, "summary": "invoice request"}'
ticket = parser.parse(raw)
print(ticket.urgency + 1) # 3
This prints 3. No network, no key, no cost. Do this for every schema you define — most parsing bugs are your schema's fault, not the model's, and you can find them in milliseconds instead of seconds.
Ten-second check
Why is a Pydantic parser better than writing your own regex to pull out JSON?
Because the field definitions, the instructions sent to the model, and the validation all come from one class. Change a field once and everything follows. And when validation fails it raises, instead of quietly passing bad data downstream.