LangChain Output Parsers: Getting Structured Data from LLMs
Why your LLM output keeps breaking your app
If you have ever tried to wire an LLM into a real application, you have probably hit this wall: the model writes beautiful, human-readable prose, and your code needs a JSON object with three specific keys. You ask nicely in the prompt. Sometimes it works. Then a user sends a slightly different input, the model adds a friendly preamble like "Sure, here's the data you asked for," and your json.loads() call throws an exception in production.
This is not a rare edge case — it's the default behavior of language models. LLMs are trained to be helpful conversationalists first and API endpoints second. Getting them to reliably return structured data that your downstream code can parse is one of the first real engineering problems you hit once you move past toy chatbot demos into building actual products: extracting fields from resumes, classifying support tickets, populating database rows, or calling other functions based on model output.
LangChain's output parser system exists to solve exactly this problem. It gives you a consistent way to tell the model what shape of data you want, and a consistent way to turn whatever text comes back into a Python object you can actually use — with validation, error messages, and retry logic baked in. In this guide, we will go through how output parsers work under the hood, build real parsers with PydanticOutputParser and StructuredOutputParser, handle the inevitable parsing failures, and look at where parsers fit next to newer approaches like function calling and with_structured_output().
What an output parser actually does
An output parser in LangChain has two jobs, and it's worth separating them clearly because people often only think about the second one.
Job one: generate format instructions. Every output parser can produce a block of text describing exactly how the model should format its response — field names, types, nesting, delimiters. You inject this text into your prompt template so the model knows the contract it needs to follow. This is the part that actually improves your success rate, because you are no longer hoping the model guesses the right format — you are telling it explicitly, in a format it has seen thousands of times in training data (JSON schemas are everywhere in a model's training corpus).
Job two: parse the raw string. Once the model responds, the parser takes the raw text output and converts it into a structured Python object — a dictionary, a Pydantic model instance, a list, whatever you defined. If the text doesn't match what was expected, the parser raises an exception (usually OutputParserException) rather than silently returning garbage.
Here is the interface in its simplest form, using the built-in CommaSeparatedListOutputParser to see the pattern before we get into the more powerful parsers:
from langchain.output_parsers import CommaSeparatedListOutputParser
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
parser = CommaSeparatedListOutputParser()
format_instructions = parser.get_format_instructions()
prompt = PromptTemplate(
template="List five {subject}.\n{format_instructions}",
input_variables=["subject"],
partial_variables={"format_instructions": format_instructions},
)
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
chain = prompt | model | parser
result = chain.invoke({"subject": "programming languages for backend development"})
print(result)
# ['Python', 'Java', 'Go', 'Rust', 'C#']Notice the three pieces working together: get_format_instructions() tells the model what to do, the prompt template injects those instructions, and the parser at the end of the chain converts the raw string into a Python list. This same three-part pattern — instructions, prompt, parse — is what every output parser in LangChain follows, no matter how complex the target schema gets.
Building your first PydanticOutputParser
The comma-separated list parser is fine for toy examples, but real applications need nested objects, type validation, optional fields, and enums. That's where PydanticOutputParser earns its keep. You define a Pydantic model describing exactly the shape of data you want, and LangChain handles turning that model into format instructions and turning the model's response back into a validated instance of that class.
Let's build something realistic: extracting structured information from a job posting.
from typing import List, Optional
from pydantic import BaseModel, Field
from langchain.output_parsers import PydanticOutputParser
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
class JobPosting(BaseModel):
title: str = Field(description="The job title as listed in the posting")
company: str = Field(description="The hiring company's name")
location: str = Field(description="City and country, or 'Remote' if fully remote")
salary_min: Optional[int] = Field(
default=None, description="Minimum salary in USD, if mentioned"
)
salary_max: Optional[int] = Field(
default=None, description="Maximum salary in USD, if mentioned"
)
required_skills: List[str] = Field(
description="List of required technical skills mentioned in the posting"
)
seniority: str = Field(
description="One of: 'junior', 'mid', 'senior', 'staff', 'principal'"
)
parser = PydanticOutputParser(pydantic_object=JobPosting)
prompt = PromptTemplate(
template=(
"Extract structured job posting details from the text below.\n"
"{format_instructions}\n"
"Job posting text:\n{posting_text}\n"
),
input_variables=["posting_text"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
chain = prompt | model | parser
posting_text = """
We're hiring a Senior Backend Engineer at Cascade Data, fully remote.
Compensation ranges from $140,000 to $175,000 depending on experience.
You should be comfortable with Python, PostgreSQL, Docker, and AWS.
"""
result = chain.invoke({"posting_text": posting_text})
print(result)
print(type(result))
print(result.required_skills)Running this gives you a real JobPosting object — not a dictionary, not a raw string — with typed, validated fields:
title='Senior Backend Engineer' company='Cascade Data' location='Remote' salary_min=140000 salary_max=175000 required_skills=['Python', 'PostgreSQL', 'Docker', 'AWS'] seniority='senior'
<class '__main__.JobPosting'>
['Python', 'PostgreSQL', 'Docker', 'AWS']Two things are worth calling out here. First, Field(description=...) is not just documentation — LangChain uses those descriptions when it builds the format instructions, so the model literally sees the schema and field descriptions as part of its prompt. Write clear descriptions and your extraction accuracy goes up noticeably, especially for ambiguous fields like seniority where you're asking the model to normalize free text into a fixed vocabulary.
Second, because JobPosting is a real Pydantic model, you get validation for free. If you had typed salary_min: int (not Optional[int]) and the posting had no salary mentioned, Pydantic would raise a validation error rather than silently letting None slip through as a string. This is the core value proposition of PydanticOutputParser: your schema is also your validation layer.
Nested models and lists of objects
Real-world extraction tasks rarely stop at flat objects. You'll often need to pull out a list of structured items, each with their own nested fields. PydanticOutputParser handles this without any special-casing — it's just standard Pydantic model composition.
from typing import List
from pydantic import BaseModel, Field
from langchain.output_parsers import PydanticOutputParser
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
class LineItem(BaseModel):
description: str = Field(description="What the line item is for")
quantity: int = Field(description="Quantity ordered")
unit_price: float = Field(description="Price per unit in USD")
class Invoice(BaseModel):
invoice_number: str = Field(description="The invoice identifier")
vendor: str = Field(description="Name of the vendor issuing the invoice")
line_items: List[LineItem] = Field(description="All billed line items")
total_due: float = Field(description="Total amount due in USD")
parser = PydanticOutputParser(pydantic_object=Invoice)
prompt = PromptTemplate(
template=(
"Parse the following invoice text into structured data.\n"
"{format_instructions}\n"
"Invoice text:\n{invoice_text}\n"
),
input_variables=["invoice_text"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
chain = prompt | model | parser
invoice_text = """
Invoice #INV-3391 from BrightPixel Studios.
2x Logo Design at $450.00 each
1x Brand Guidelines PDF at $300.00
Total due: $1200.00
"""
result = chain.invoke({"invoice_text": invoice_text})
for item in result.line_items:
print(item.description, item.quantity, item.unit_price)
print("Total:", result.total_due)The format instructions LangChain generates for nested models include the full JSON schema, so the model sees exactly how line_items should be structured as an array of objects, each matching the LineItem shape. This is significantly more reliable than trying to describe nested JSON in a hand-written prompt, because the schema is generated programmatically from your actual class definitions — if you rename a field in LineItem, the format instructions update automatically the next time you call get_format_instructions().
StructuredOutputParser: a lighter-weight alternative
Sometimes defining a full Pydantic model is more ceremony than you need — maybe you're prototyping, or the schema is genuinely simple and flat, or you're working in a codebase that avoids Pydantic for stylistic reasons. StructuredOutputParser gives you the same instructions-plus-parsing pattern using plain ResponseSchema objects instead of a class definition.
from langchain.output_parsers import StructuredOutputParser, ResponseSchema
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
response_schemas = [
ResponseSchema(name="sentiment", description="One of: positive, negative, neutral"),
ResponseSchema(name="confidence", description="A float between 0 and 1"),
ResponseSchema(
name="key_phrases",
description="A list of short phrases that justify the sentiment",
),
]
parser = StructuredOutputParser.from_response_schemas(response_schemas)
format_instructions = parser.get_format_instructions()
prompt = PromptTemplate(
template="Analyze the sentiment of this review.\n{format_instructions}\nReview:\n{review}\n",
input_variables=["review"],
partial_variables={"format_instructions": format_instructions},
)
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
chain = prompt | model | parser
result = chain.invoke({
"review": "The battery life is disappointing but the camera quality is genuinely excellent."
})
print(result)
# {'sentiment': 'neutral', 'confidence': 0.7, 'key_phrases': ['disappointing battery life', 'excellent camera quality']}StructuredOutputParser returns a plain dictionary rather than a typed object, which is the main tradeoff versus PydanticOutputParser. You lose type validation and IDE autocompletion on the result, but you gain a faster setup path when you don't need strict typing — useful for quick scripts, notebook experimentation, or schemas that genuinely are just a flat bag of key-value pairs.
A practical rule of thumb: reach for StructuredOutputParser when you're iterating quickly or the output feeds directly into something like a dictionary-based API payload. Reach for PydanticOutputParser the moment you need validation, nested structures, enums, or the result is going to be passed around your codebase as a typed object rather than consumed immediately.
Handling parsing failures gracefully
No matter how good your format instructions are, models occasionally misbehave — they might wrap JSON in markdown code fences, add a stray sentence before the JSON, drop a required field, or hallucinate a field name. When PydanticOutputParser.parse() receives text it can't map onto your schema, it raises an OutputParserException. You need a strategy for this rather than letting it crash your pipeline.
LangChain gives you two built-in ways to recover: OutputFixingParser and RetryOutputParser.
OutputFixingParser wraps your existing parser and, on failure, sends the broken output back to an LLM with instructions to fix it so it matches the schema:
from langchain.output_parsers import OutputFixingParser, PydanticOutputParser
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
class Contact(BaseModel):
name: str = Field(description="Full name of the contact")
email: str = Field(description="Email address")
phone: str = Field(description="Phone number, digits only")
base_parser = PydanticOutputParser(pydantic_object=Contact)
# Imagine the model returned malformed JSON, e.g. missing quotes or a trailing comma
malformed_output = '{"name": "Priya Sharma", "email": "priya@example.com", "phone": "9876543210",}'
fixing_parser = OutputFixingParser.from_llm(
parser=base_parser,
llm=ChatOpenAI(model="gpt-4o-mini", temperature=0),
)
try:
result = base_parser.parse(malformed_output)
except Exception:
result = fixing_parser.parse(malformed_output)
print(result)
# name='Priya Sharma' email='priya@example.com' phone='9876543210'RetryOutputParser goes a step further: instead of just fixing the malformed text, it re-invokes the original prompt along with the broken completion, giving the model full context about what it was asked to do and where it went wrong. This tends to produce better results than OutputFixingParser when the original output wasn't just malformed but semantically wrong (like missing a field entirely), since the model gets to see the original instructions again rather than just patching bad JSON.
from langchain.output_parsers import RetryOutputParser
from langchain_openai import ChatOpenAI
retry_parser = RetryOutputParser.from_llm(
parser=base_parser,
llm=ChatOpenAI(model="gpt-4o-mini", temperature=0),
)
# retry_parser.parse_with_prompt(completion, original_prompt_value) requires
# access to the original PromptValue so it can show the model its own instructions againIn production, a common pattern is to chain these as a fallback ladder: try the strict parser first, fall back to OutputFixingParser on failure, and only escalate to logging/alerting if both fail. This keeps your success rate high without silently accepting bad data.
Custom parsers for domain-specific formats
Sometimes neither Pydantic schemas nor response schemas fit what you need — maybe you're parsing a custom DSL, a specific log format, or output that needs post-processing beyond basic type coercion. LangChain lets you build a fully custom parser by subclassing BaseOutputParser.
from langchain_core.output_parsers import BaseOutputParser
from typing import List
class BulletListParser(BaseOutputParser[List[str]]):
"""Parses markdown-style bullet lists into a Python list of strings."""
def parse(self, text: str) -> List[str]:
lines = text.strip().split("\n")
items = []
for line in lines:
stripped = line.strip()
if stripped.startswith(("- ", "* ")):
items.append(stripped[2:].strip())
if not items:
raise ValueError(f"No bullet items found in output: {text!r}")
return items
def get_format_instructions(self) -> str:
return (
"Respond with a markdown bullet list, one item per line, "
"using '- ' at the start of each line. Do not include any other text."
)
@property
def _type(self) -> str:
return "bullet_list"This is a small example, but the pattern scales: any time you can describe "here is the text format I want" and "here is the deterministic Python logic to turn that text into a data structure," you can wrap it as a BaseOutputParser subclass and drop it into a chain exactly like the built-in parsers. This is especially useful when you're parsing output that mixes structured and unstructured content — for example, a model that writes a short explanation followed by a fenced code block, where you want to split those into two separate fields without asking the model to produce full JSON (which sometimes makes multi-paragraph text awkward to extract cleanly).
Output parsers versus native structured output
It's worth being upfront about where the ecosystem is heading. Modern chat models increasingly support structured output natively — OpenAI's function calling and JSON mode, Anthropic's tool use, and LangChain's own with_structured_output() method, which binds a Pydantic schema directly to the model call and uses the provider's native mechanism (tool calling or JSON mode) rather than relying purely on prompt instructions.
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
class Person(BaseModel):
name: str = Field(description="The person's full name")
age: int = Field(description="The person's age in years")
occupation: str = Field(description="The person's job or profession")
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
structured_model = model.with_structured_output(Person)
result = structured_model.invoke(
"Tell me about a fictional character: Marcus Webb, a 34-year-old architect."
)
print(result)
# name='Marcus Webb' age=34 occupation='architect'When it's available for your model provider, with_structured_output() is generally the better default for new projects — it pushes schema enforcement down to the API level, which tends to be more reliable than pure prompt-based instructions, and it requires less boilerplate.
So where does that leave PydanticOutputParser and StructuredOutputParser? They're still genuinely useful in a few situations: when you're working with models or providers that don't support native structured output or tool calling, when you need output parsing as an explicit, inspectable step in a longer chain (for example, alongside custom pre- or post-processing logic), when you're building parsers for text that isn't purely a single structured call — like extracting structured data buried inside a longer freeform explanation — and when you want full control over the exact prompt wording the model sees, rather than delegating schema communication to the provider's internal mechanism. Understanding output parsers also just makes you a better LangChain developer, because the underlying concepts — format instructions, parsing, validation, retry — show up throughout the framework even when the specific parser classes aren't in play.
Practical tips for production use
A few lessons that show up repeatedly once you take output parsers from a notebook into a real system.
- Keep your Pydantic field descriptions specific and example-driven. Vague descriptions like "the category" produce vague extractions; descriptions like "one of: refund, shipping, technical, billing, other" produce far more consistent categorization.
- Set
temperature=0for extraction tasks. You want the model to be deterministic and literal, not creative, when it's filling in a schema. - Always wrap
.parse()calls (or your full chain.invoke()) in error handling in production code, even when you're usingOutputFixingParser— a fixing parser can still fail if the underlying LLM call fails entirely. - Test your parser against edge cases in your actual data, not just clean examples. Try inputs with missing information, ambiguous phrasing, and unusual formatting to see how your schema and prompt hold up.
- When using nested models or lists, keep the nesting as shallow as reasonably possible. Deeply nested schemas increase the chance of the model dropping or malforming an inner field, especially with smaller or cheaper models.
- Log both the raw model output and the parsed result during development. When a parser throws an exception, you want to see exactly what text caused it, not just the exception message.
Wrapping up
Output parsers are one of those unglamorous pieces of infrastructure that make the difference between an LLM demo and an LLM-powered product people can actually rely on. PydanticOutputParser gives you typed, validated, nested data structures straight out of a language model call. StructuredOutputParser gives you a faster path to the same idea when a full class definition is overkill. OutputFixingParser and RetryOutputParser give you a safety net for the inevitable malformed response. And when you're on a provider that supports it, with_structured_output() gives you an even more robust native alternative.
None of these tools are exotic — they're all thin, well-designed wrappers around ideas you'd eventually build yourself: describe the schema, ask for it explicitly, validate what comes back, retry on failure. Knowing which tool fits which situation is what separates code that works in a demo from code that survives contact with real users and real, messy inputs.
If you want to go deeper — building multi-step chains that combine parsers with retrieval, memory, and agents, and seeing these patterns applied across production-grade projects — our LangChain Tutorial 2026 course on teachyou.ai walks through all of it hands-on, from first principles through to deployable applications.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.