Output parsers
A model returns text; your program needs data. A parser is the boundary where that text becomes a value you can trust — or an error you can handle.
Text is not a data structure
The moment a reply is used by code rather than shown to a person, it has to be parsed. “The sentiment is positive” is a sentence; {"sentiment": "positive"} is a value you can branch on, count and store. The parser is the step that makes that conversion explicit.
Doing the conversion implicitly — a regular expression here, a .split() there, scattered through the application — is how a formatting change in the prompt breaks three unrelated features. One parser, in one place, with one set of tests.
A worked failure: a reply is parsed with a regular expression in one place and split on a colon in another. The prompt later changes from “Sentiment: positive” to a JSON object, and only one of the two parsers breaks. The feature that still works hides the one that does not, so the bug surfaces days later in a report. One parser with one test would have failed at the change.
Ask for the shape you intend to parse
Parsers work best when the prompt states the format exactly: which keys, which types, whether extra text is allowed. If the prompt says “reply in JSON” and nothing else, the model may add a friendly sentence before the object, wrap it in a code fence, or invent a key.
A schema in the prompt and a parser in the code are two halves of the same contract. The parser should reject what the prompt did not promise, and the prompt should be written with the parser’s strictness in mind.
A parser that declares its own format
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field
class Verdict(BaseModel):
sentiment: str = Field(description="positive, negative or neutral")
confidence: float = Field(description="between 0 and 1")
parser = JsonOutputParser(pydantic_object=Verdict)
prompt = ChatPromptTemplate.from_messages([
("system", "Reply only with JSON.\n{format}"),
("human", "{text}"),
]).partial(format=parser.get_format_instructions())
chain = prompt | model | parser
print(chain.invoke({"text": "The update broke everything."}))Parse labelled lines
The parser reads `key: value` lines. Feed it a reply with a missing colon and a duplicate key, and decide which of the two should be an error.
Strictness is a choice you document
Decide what the parser accepts and write it down. A duplicate key can be an error, a last-wins overwrite, or a first-wins keep; each is defensible, and none is obvious to the next reader. The same goes for a missing optional field: absent and null are different values, and the rest of the program will treat them differently.
A common mistake is a lenient parser that silently repairs the reply. It lowercases a label, strips a prefix, coerces a number from a string. Each repair hides a prompt that is drifting. The symptom is a parser that never fails and a downstream value that is occasionally wrong; log the repairs and treat a rising count as a prompt bug.
Every parser needs a failure path
A parser will eventually meet something it cannot read: a truncated reply, a code fence, an apology instead of an object. What happens next is a design decision. Retrying with the error message appended is often enough; refusing and surfacing the raw text is always acceptable; silently returning an empty value is not.
The worst failure is a lenient parser that guesses. {"sentiment": "pos"} becoming positive is fine if the mapping is written down, and dangerous if it is a side effect of prefix matching that also turns positive-ish, maybe negative into positive.
Give the retry a small budget — two attempts is usually enough — and log the raw reply on the final failure. A parser that retries forever turns a formatting problem into an outage, and the log is the only thing that survives it. The retry budget belongs beside the parser, not in the caller, so every path through the code gets the same policy.
Never let parsing exceptions disappear into a bare except. The raw reply is the only evidence of what went wrong, and it is gone the moment the exception is swallowed.
Parse once, then pass objects
After the parser, the rest of the program should never see the raw string again. A dictionary or a typed object can be validated, logged and tested; the text it came from is an implementation detail of the model call.
That boundary also gives you a place to record the failures that matter: how often parsing fails is a quality signal about the prompt, and a number worth watching rather than a surprise in production.
JSON in a code fence
Models often wrap JSON in a fence. Strip it robustly, parse it, and return a clear error when the text is not JSON at all.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Why parse a reply explicitly instead of using it as text?
