Output Parsers & Structured Output
An LLM replies with text, but your code needs a list, a dict or an object it can save to a database. Output parsers convert text into Python data, and with_structured_output makes the model fill your schema directly. In this lab you extract phone details, movie facts and a fraud-risk report from a bank SMS.
In this session
- Why parsers: text in, Python data out
- String, comma list, numbered and markdown list parsers
- JsonOutputParser and PydanticOutputParser
- Format instructions inside the prompt
- OutputFixingParser repairs broken output
- with_structured_output: the recommended way
01The big idea: from paragraph to database row
Think of a bank clerk filling a form from a customer’s story. The customer talks freely (“I paid forty-nine thousand at 3 AM to some loan app…”), but the system only accepts fields: amount, merchant, channel, risk. The parser is that clerk: it turns free text into exact fields, and rejects “eighty thousand” where a number is required.
02The modern way: with_structured_output
Older parsers ask the model nicely (format instructions in the prompt) and hope. llm.with_structured_output(Schema) uses the model’s native tool calling / JSON mode, so the reply always matches your schema and comes back as a Pydantic object:
from typing import Literal
from pydantic import BaseModel
class TransactionRisk(BaseModel):
amount_inr: float
merchant: str
channel: Literal["UPI", "card", "netbanking", "unknown"]
risk_level: Literal["LOW", "MEDIUM", "HIGH"]
reasons: list[str]
risk_llm = llm.with_structured_output(TransactionRisk)
result = risk_llm.invoke("UPI payment of Rs 49,999 to 'QuickLoanzz' at 3:12 AM from a new device, right after a password reset.")
print(result.risk_level, result.reasons)| Parser | Output | Use when |
|---|---|---|
StrOutputParser | str | chat answers, end of most chains |
CommaSeparatedList / NumberedList | list | simple lists |
JsonOutputParser | dict | quick JSON, flexible keys |
PydanticOutputParser | object | validation needed, model has no tool calling |
with_structured_output | object / dict | best default for modern models |
OutputFixingParser | object | repair broken output with a second LLM call |
Literal["LOW", "MEDIUM", "HIGH"] limits the model to exact values, so your if result.risk_level == "HIGH" code never meets “high-ish”.03Hands-on: run it here
The parsers run on the same kind of text a real LLM returns. Watch the Pydantic parser reject "eighty thousand" for an int field, then the structured-output call return a typed object.
from standins import RuleModel, call
from pydantic import BaseModel, Field
from typing import Literal
from langchain_core.output_parsers import (StrOutputParser, CommaSeparatedListOutputParser,
JsonOutputParser, PydanticOutputParser)
from langchain_core.exceptions import OutputParserException
# The same text a real LLM would send back
print(CommaSeparatedListOutputParser().parse("Python, JavaScript, Java, C++, Go"))
print(JsonOutputParser().parse('```json\n{"name": "Pixel 9", "price": 79999, "features": ["camera", "AI", "battery"]}\n```'))
class Smartphone(BaseModel):
name: str = Field(description="model name")
price: int = Field(description="price in INR")
features: list[str] = Field(description="top 3 features")
parser = PydanticOutputParser(pydantic_object=Smartphone)
phone = parser.parse('{"name": "Galaxy S24", "price": 74999, "features": ["AI camera", "120Hz", "7 years updates"]}')
print(repr(phone), "| price + GST:", round(phone.price * 1.18))
try: # wrong type -> validation error instead of bad data in your DB
parser.parse('{"name": "iPhone 16", "price": "eighty thousand", "features": ["camera"]}')
except OutputParserException as e:
print("rejected:", str(e).splitlines()[0][:80])
# with_structured_output: the model fills the schema through tool calling
class TransactionRisk(BaseModel):
amount_inr: float
merchant: str
channel: Literal["UPI", "card", "netbanking", "unknown"]
risk_level: Literal["LOW", "MEDIUM", "HIGH"]
reasons: list[str]
llm = RuleModel(default=call("TransactionRisk", {"amount_inr": 49999, "merchant": "QuickLoanzz", "channel": "UPI",
"risk_level": "HIGH", "reasons": ["3 AM transaction", "new device", "right after password reset"]}))
result = llm.with_structured_output(TransactionRisk).invoke("UPI payment of Rs 49,999 to 'QuickLoanzz' at 3:12 AM from a new device, right after a password reset.")
print(type(result).__name__, result.risk_level, result.reasons)RuleModel or ScriptedModel from standins.py), so every output below is from a real run of this code. In Colab you swap one line: llm = ChatGroq(model="openai/gpt-oss-20b").04Hands-on: Ajeetkumar’s Colab notebook
groq_key with your free key from console.groq.com, switch on Notebook access, then run the cells from the top.05Practice tasks
- Extract
name, email, phonefrom a job application text withwith_structured_output. - Get 10 Indian cities as a Python list with
CommaSeparatedListOutputParser. - Make a
Recipe(name, ingredients: list[str], minutes: int)Pydantic parser.
Try these in the same Colab notebook after you finish the lab. Change one thing at a time and look at the output again.
06Summary
- LLMs return text; parsers turn it into lists, dicts and objects.
- Format instructions tell the model the shape; Pydantic validates types.
OutputFixingParsercan repair broken output with another LLM call.with_structured_output(Schema)is the most reliable way on modern models.- Use
Literalfor fixed choices like risk levels.
07Quick quiz
1. Best default for structured data on a model with tool calling?
2. What does PydanticOutputParser add over JsonOutputParser?
3. "price": "eighty thousand" for an int field. Result?
4. What does get_format_instructions() do?
5. Why use Literal["LOW","MEDIUM","HIGH"]?
08References
- LangChain: Structured output
- Pydantic docs
- Ajeetkumar’s notebook: LC_03_Output_Parsers_and_Structured_Output.ipynb (download)