n4nAI

LangChain with_structured_output explained step by step

Hands-on langchain with_structured_output tutorial: define Pydantic schemas, bind to chat models, parse LLM responses, and avoid common pitfalls.

n4n Team3 min read633 words

Audio narration

Coming soon — every post will get a voice note here.

In this langchain with_structured_output tutorial we go from zero to a typed LLM extraction pipeline that never hands you a bare string when you expected an object. You’ll define a Pydantic schema, bind it to a chat model, and handle the edge cases that bite in production.

Prerequisites

  • Python 3.10 or newer
  • langchain-openai and pydantic v2 installed
  • An OpenAI API key, or any OpenAI-compatible endpoint (see note below)
pip install langchain-openai pydantic

Set your key:

export OPENAI_API_KEY="sk-..."

If you route through an OpenAI-compatible gateway such as n4n.ai, set base_url and the gateway key instead; with_structured_output works identically because the gateway forwards the native structured output parameters.

Step 1: Define the output contract

The first rule of any langchain with_structured_output tutorial is to never trust the model’s shape. Write a Pydantic model that encodes exactly what you need.

from pydantic import BaseModel, EmailStr, Field

class Contact(BaseModel):
    name: str = Field(description="Full name of the person")
    age: int = Field(description="Age in years", ge=0, le=120)
    emails: list[EmailStr] = Field(description="Verified email addresses")

The description fields are not cosmetic—LangChain forwards them as JSON schema descriptions, and many providers use them to steer generation.

Step 2: Bind the schema to a chat model

Create a standard ChatOpenAI instance and call with_structured_output. This returns a new runnable that accepts the same inputs but emits validated objects.

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
structured_llm = llm.with_structured_output(Contact)

Under the hood, LangChain inspects the model’s capabilities. For OpenAI models it uses function-calling or JSON mode; for models without native support it falls back to a prompt + parser combination, which is strictly worse. Always verify your model supports native structured output.

Step 3: Invoke and inspect

Run a simple extraction:

text = "Sara Connor, 34, reachable at sara@example.com or s.connor@example.org"
result = structured_llm.invoke(text)
print(result)

Expected output:

name='Sara Connor' age=34 emails=['sara@example.com', 's.connor@example.org']

You get a Contact instance, not a dict. That means .emails[0] is typed and EmailStr has already validated the addresses.

Step 4: Get the raw response when you need it

In production you often want the token usage or the original message. Pass include_raw=True:

structured_with_raw = llm.with_structured_output(Contact, include_raw=True)
raw = structured_with_raw.invoke(text)
print(type(raw))
print(raw["parsed"].name)
print(raw["raw"].usage)

raw is a dict with keys parsed (your object or None) and raw (the AIMessage). If validation fails, parsed is None and you can inspect raw to see what the model actually returned.

Step 5: Use a JSON schema instead of Pydantic

As you progress through this langchain with_structured_output tutorial, you’ll notice dict schemas are interchangeable with Pydantic for the binding step. Some teams keep schemas as plain dicts.

json_schema = {
    "title": "Contact",
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age": {"type": "integer", "minimum": 0},
        "emails": {"type": "array", "items": {"type": "string", "format": "email"}}
    },
    "required": ["name", "age", "emails"]
}

structured_from_dict = llm.with_structured_output(json_schema)
print(structured_from_dict.invoke(text))

This yields the same object, but you lose Pydantic’s post-validation coercions (e.g., EmailStr checking). Use Pydantic unless you have a hard constraint.

Step 6: Handle multiple records

Models can return lists if you ask. Define a wrapper:

from typing import List

class ContactList(BaseModel):
    contacts: List[Contact]

list_llm = llm.with_structured_output(ContactList)
multi = list_llm.invoke(
    "Tom, 22, tom@x.com and Lucy, 41, lucy@y.com"
)
print(multi.contacts[1].name)

Expected: Lucy.

Step 7: Streaming—know the limitation

with_structured_output does not stream partial Pydantic objects. If you call .stream(), you get the full parsed object only after completion (or chunks of the raw string if include_raw=True with certain providers). For token-by-token UX, stream the base LLM and parse client-side, or accept that structured output is a batch operation.

# Does NOT yield incremental Contact objects
for chunk in structured_llm.stream(text):
    print(chunk)  # prints full Contact once at end

Step 8: Compose with a prompt template

You rarely call the model directly. Wrap with a ChatPromptTemplate:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "Extract contact info from the user text."),
    ("user", "{input}")
])
chain = prompt | structured_llm
out = chain.invoke({"input": text})
print(out.name)

This is the pattern you’ll actually deploy: prompt engineering on the left, typed parsing on the right.

Step 9: Validation and retries

If include_raw returns parsed=None, retry with a clearer instruction:

def extract(text):
    res = structured_with_raw.invoke(text)
    if res["parsed"] is None:
        res = structured_with_raw.invoke(f"Strictly output JSON. {text}")
    return res["parsed"]

print(extract("Bob, 50, bob@corp.net"))

For robust pipelines, use RunnableRetry from langchain_core, but the above shows the mechanics.

Common pitfalls

Pydantic v1 vs v2

LangChain >= 0.2 expects Pydantic v2. If you see TypeError: cannot use with_structured_output with pydantic v1, upgrade or use from pydantic.v1 import BaseModel only if the docs explicitly allow.

Temperature > 0

Structured output is deterministic by construction, but a high temperature can increase malformed JSON rates on fallback parsers. Keep temperature=0 for extraction.

Missing required fields

If the text lacks a field, the model may hallucinate. Constrain with Field(default=None) if optional, or catch ValidationError from include_raw.

Provider mismatch

Not every model behind an OpenAI-compatible endpoint supports response_format={"type":"json_object"}. Test before shipping.

Wrapping up

This langchain with_structured_output tutorial showed the shortest path from a prompt to a validated object: define schema, bind, invoke. The pattern replaces ad-hoc JSON parsing with a compile-time contract. For anything beyond toys, prefer Pydantic, keep temperature at zero, and wrap calls with include_raw=True so you can observe failures instead of swallowing them.

Tagslangchainstructured-outputpydantictutorial

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All langchain structured output & parsers posts →