LangChain Hands-on · Module 1 · Lab 03

Output Parsers & Structured Output

⏱ 20 min + hands-on🎯 Beginner🖼 1 diagram🧪 2 live demos🐍 Python code

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.

LLM text“Python, Java, Go”ParserCommaSeparatedListPython data['Python','Java','Go']Your appDB, UI, API
A parser is the last step of a chain: it makes the output usable by code.
Demo · Parser playground: pick a sample, change the text

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:

fraud_check.py
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)
ParserOutputUse when
StrOutputParserstrchat answers, end of most chains
CommaSeparatedList / NumberedListlistsimple lists
JsonOutputParserdictquick JSON, flexible keys
PydanticOutputParserobjectvalidation needed, model has no tool calling
with_structured_outputobject / dictbest default for modern models
OutputFixingParserobjectrepair broken output with a second LLM call
Tip: 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.

lab03_parsers.py
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)
['Python', 'JavaScript', 'Java', 'C++', 'Go'] {'name': 'Pixel 9', 'price': 79999, 'features': ['camera', 'AI', 'battery']} Smartphone(name='Galaxy S24', price=74999, features=['AI camera', '120Hz', '7 years updates']) | price + GST: 88499 rejected: Failed to parse Smartphone from completion {"name": "iPhone 16", "price": "eight TransactionRisk HIGH ['3 AM transaction', 'new device', 'right after password reset']
Runs here without any API key. A small stand-in plays the LLM (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

Colab setup (same in every notebook): click 🔑 Secrets in the Colab sidebar, add groq_key with your free key from console.groq.com, switch on Notebook access, then run the cells from the top.
Demo · Notebook player · Lab 03

05Practice tasks

  1. Extract name, email, phone from a job application text with with_structured_output.
  2. Get 10 Indian cities as a Python list with CommaSeparatedListOutputParser.
  3. 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

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

Created by Ajeetkumar