Dissecting output parsers in LangChain: StrOutputParser, JsonOutputParser, PydanticOutputParser, and with_structured_output to force the model to return a structured format, complete with streaming parsing techniques for partial output.

In episode 5 you assembled your first chain with LCEL: chain = prompt | model | parser. That chain works because every component is a Runnable, and the output parser is one of the important members of that chain. You also got a taste of RunnableParallel and RunnablePassthrough for manipulating the data flow between components.
This episode focuses on one element that's often underestimated but determines application quality: the output parser. Language models return free text, while your applications frequently need structured data. We'll dissect four strategies: StrOutputParser for plain text, JsonOutputParser for JSON, PydanticOutputParser for validated objects, and with_structured_output, which hands the formatting task directly to the model. Finally, we'll learn how to parse streams of streamed chunks.
Without a parser, every model response is a free-form string you have to parse yourself — and parsing free text is fragile. Output parsers add a contract layer: the chain always returns the same type, so the calling code doesn't have to guess.
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o-mini")
hasil = model.invoke("Sebutkan tiga bahasa pemrograman populer")
print(hasil.content)The end of the chain is where each parser makes its main difference. There are four families we'll use over and over: StrOutputParser converts AIMessage to str, JsonOutputParser converts a JSON string to dict, PydanticOutputParser converts JSON to a BaseModel instance, and with_structured_output makes the model produce JSON matching the requested schema.
The simplest and most widely used parser. It takes the content from the model output — usually an AIMessage — and returns it as a pure string.
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
prompt = ChatPromptTemplate.from_messages([
("system", "Jawab dalam bahasa Indonesia, maksimal dua kalimat."),
("human", "{pertanyaan}"),
])
model = ChatOpenAI(model="gpt-4o-mini")
chain = prompt | model | StrOutputParser()
jawaban = chain.invoke({"pertanyaan": "Apa itu vector database?"})
print(jawaban)Notice that chain.invoke() now returns a str, not an AIMessage. This simplifies the calling code: other functions can consume jawaban directly without knowing anything about message structure. StrOutputParser can also be placed at the end of more complex chains, for example after a RunnableParallel whose results you want combined into a single text.
When you need structured data like key-value pairs, JsonOutputParser is a practical choice. Its advantage: it knows how to handle the partial JSON that comes out during streaming, because this parser runs per-character logic using an internal parser tolerant of incomplete tokens.
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
parser = JsonOutputParser()
prompt = PromptTemplate(
template="Jawab dalam JSON. {format_instructions}\nPertanyaan: {pertanyaan}",
input_variables=["pertanyaan"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
model = ChatOpenAI(model="gpt-4o-mini")
chain = prompt | model | parser
data = chain.invoke({"pertanyaan": "Data ibu kota Indonesia"})
print(data["ibu_kota"])get_format_instructions() injects the format instructions into the prompt so the model knows exactly the expected JSON structure. In the example above, the result is a Python dict so you can directly access data["ibu_kota"] without regex or string slicing.
Info
JsonOutputParser is not a schema validator. It only ensures the output is parseable JSON. If you need strict validation of specific fields, move up to Pydantic.
For a stricter contract, use PydanticOutputParser. It combines JSON parsing with Pydantic validation: the output schema is derived from a BaseModel definition, and wrong-typed fields immediately raise a ValidationError.
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import PromptTemplate
from pydantic import BaseModel, Field
class Ringkasan(BaseModel):
judul: str = Field(description="Judul artikel")
poin_utama: list[str] = Field(description="Poin-poin penting")
parser = PydanticOutputParser(pydantic_object=Ringkasan)
prompt = PromptTemplate(
template="Ringkas teks berikut dalam JSON. {format_instructions}\nTeks: {teks}",
input_variables=["teks"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
model = ChatOpenAI(model="gpt-4o-mini")
chain = prompt | model | parser
hasil = chain.invoke({"teks": "LangChain adalah framework untuk membangun aplikasi LLM..."})
print(hasil.judul)
print(hasil.poin_utama)The main advantage here is type safety: hasil.judul is guaranteed to be a str and hasil.poin_utama a list[str]. Typo errors in the JSON output are caught at validation time, not in the middle of your application logic.
The most modern approach: instead of parsing after the response arrives, we ask the model to return a specific format from the start. with_structured_output maps the schema to the provider's built-in tool calling or response format, so the result is more consistent than relying on text instructions in the prompt.
from dataclasses import dataclass
from langchain_openai import ChatOpenAI
@dataclass
class Klasifikasi:
label: str
skor: float
model = ChatOpenAI(model="gpt-4o-mini")
model_terstruktur = model.with_structured_output(Klasifikasi)
hasil = model_terstruktur.invoke(
"Teks ini tentang database vector: skor sentimen dan label temanya"
)
print(hasil.label, hasil.skor)with_structured_output accepts Pydantic classes, dataclasses, or TypedDict. When given a dataclass, LangChain performs automatic coercion to the Python types you define. The result isn't raw JSON, but an object ready to be used in your domain code.
Success
For production chains that need deterministic output, prioritize with_structured_output or PydanticOutputParser. Manually parsing free text only adds points of failure.
In streaming mode, output arrives in parts. Text-based parsers like StrOutputParser can be used directly; for JSON, JsonOutputParser produces partial objects that are still valid when parsed partially. This matters for a responsive user experience.
chain = prompt | model | StrOutputParser()
async for potongan in chain.astream({"pertanyaan": "Jelaskan RAG secara singkat"}):
print(potongan, end="", flush=True)Because StrOutputParser just passes text through, each potongan above is a string chunk that can be rendered directly. For JsonOutputParser, chain.astream() produces progressively more complete partial dictionaries as the data flows — just render the fields already available. For instruction-based parsers like PydanticOutputParser, full streaming isn't supported across all versions, so plain invoke or stream_events is safer for monitoring stages without sacrificing validation.
This episode turned model output from free text into structured contracts: StrOutputParser for strings, JsonOutputParser for dicts, PydanticOutputParser for validated objects, and with_structured_output for direct coercion to Python types. You also learned how to handle streamed chunks without breaking the pipeline.
Key takeaways:
StrOutputParser converts AIMessage to str — the most common endpoint in a chain.JsonOutputParser is great for quick JSON and tolerant of partial output during streaming.PydanticOutputParser provides strict validation via the BaseModel schema and automatic format instructions.with_structured_output hands JSON generation to the model via tool calling or response format, the most consistent for production.invoke.In episode 7 we deal with memory: Memory & Conversation State — storing conversation history, using thread_id, and leveraging LangGraph checkpoints so your agents remember who they're talking to. See you there!