Lesson 4 of 716 min2 challenges

Structured Output: Turning Text into Data

PydanticOutputParser, with_structured_output, and what to do when parsing fails.

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:

  1. Set temperature=0. Structured extraction has one correct answer. Randomness only costs you.
  2. 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.
  3. 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.

Ad slot (AdSense not configured; ads appear here in production)

Hands-on

Reading it isn't knowing it. Writing it is.

0/2 passed

Challenges and grading are completely free, and a wrong answer costs you nothing. Stuck? Hit "Ask the AI": it can see the code you are writing.

Level 01Multiple choice+10 pts · +15 XP

Why Pydantic instead of your own JSON slicing?

Compared with using a regex to dig JSON out of the model’s reply, what is the core benefit of PydanticOutputParser?

  • Might be multiple choice. Wrong answers cost nothing, so just try.
Level 02Multiple choice+12 pts · +15 XP

Three safeguards for production

When you rely on structured output, which of these should you do? (Select all that apply.)

  • Might be multiple choice. Wrong answers cost nothing, so just try.

Finished? Mark it

Marking it done saves your progress, gives you 20 XP and keeps your streak alive.