LangChain with_structured_output() — schema validation and method errors
<code>with_structured_output()</code> is the cleanest way to get typed responses from any chat model. It succeeds when configured correctly and fails in subtle ways when not — usually around Pydantic version, method choice, or provider-specific schema constraints.
Quick fix (TL;DR)
llm.with_structured_output(Schema) returns a runnable that parses model output into your schema. Fix errors by (a) using Pydantic v2 BaseModel (v1 is deprecated in langchain-core 0.3+), (b) picking the right method for the provider (function_calling, json_mode, or json_schema), (c) avoiding features the provider rejects (deeply nested unions, self-referential types), and (d) setting include_raw=True when debugging validation errors.Real error messages you'll see
These are the exact strings returned by the LangChain framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.
PydanticSchemaGenerationError: Unable to generate pydantic-core schema for <class 'MyModel'>. Set arbitrary_types_allowed=True in the model_config, or use a different type.
ValueError: method="json_schema" is not supported for gpt-3.5-turbo. Use "function_calling" or "json_mode".
openai.BadRequestError: 400 - The maximum depth of nested $ref is 5, but got 7.
Reference
Method options for with_structured_output
| Method | Provider support | Notes |
|---|---|---|
function_calling | OpenAI, Anthropic, Google, Bedrock | Uses tool_use; most reliable |
json_mode | OpenAI, some others | Forces JSON but no schema enforcement |
json_schema | OpenAI (Structured Outputs) | Strongest schema guarantee |
Provider quirks for structured output
| Provider | Recommended method | Notes |
|---|---|---|
| OpenAI (4o+, o1+) | json_schema | Strongest guarantees; strict mode enabled |
| OpenAI (older) | function_calling | json_schema not available |
| Anthropic Claude | function_calling | Uses tool_use blocks |
| Google Gemini | function_calling | Reliable |
| Ollama / open-source | json_mode | Sometimes brittle; add validation retry |
Root causes, ranked by frequency
Based on developer reports across LangChain forums, GitHub issues, and Discord community during 2025–2026.
- 25%Pydantic v1 model with v2 langchain-core. langchain-core 0.3+ requires Pydantic v2 exclusively.
- 18%Method not supported. Passing
method="json_schema"to a provider that does not support it. - 14%Complex nested schema rejected. Deep nesting, self-references, or complex Unions.
- 10%Field description missing. Field without
Field(description="...")— model outputs empty/wrong values. - 8%Streaming with structured output.
method="function_calling"does not stream well; users see nothing until complete. - 7%Optional field on strict provider. OpenAI strict Structured Outputs rejects Optional.
- 8%Non-Pydantic schema (raw dict). Passing an OpenAPI-style dict; some validation edge cases.
- 10%Retry loop on validation failure. Model consistently produces almost-valid JSON; parser retries and fails.
Fixes — copy-paste solutions
Use Pydantic v2 with clear field descriptions
Define your schema as a Pydantic v2 BaseModel. Every field must have Field(description="..."). Pass the class to with_structured_output.
from typing import List, Literal from pydantic import BaseModel, Field from langchain_openai import ChatOpenAI class ExtractedInfo(BaseModel): """Information extracted from the input text.""" name: str = Field(description="The person's full name") role: str = Field(description="Their job title or role") company: str = Field(description="The company they work at") years_experience: int = Field( default=0, description="Years of experience mentioned; 0 if not mentioned" ) skills: List[str] = Field( default_factory=list, description="Technical skills mentioned" ) confidence: Literal["low", "medium", "high"] = Field( default="medium", description="Your confidence in the extracted information" ) llm = ChatOpenAI(model="gpt-4o-mini") # For OpenAI 4o+, json_schema is best (strict mode) structured_llm = llm.with_structured_output(ExtractedInfo, method="json_schema", strict=True) result = structured_llm.invoke( "Alice Johnson has been a Senior ML Engineer at TechCorp for 5 years. " "She specializes in NLP and computer vision." ) print(result) # ExtractedInfo(name='Alice Johnson', role='Senior ML Engineer', # company='TechCorp', years_experience=5, # skills=['NLP', 'computer vision'], confidence='high') # Access as an object print(result.name) print(result.skills)
Field(description=...) is not decorative — it is the field-level documentation the model uses to know what to extract. Sparse descriptions produce empty or wrong values.Pick the right method per provider
Different providers support different structured-output methods with different guarantees. Choose the strongest one your provider supports.
from pydantic import BaseModel, Field from typing import List class Recipe(BaseModel): name: str = Field(description="Recipe name") ingredients: List[str] = Field(description="Ingredient list") steps: List[str] = Field(description="Cooking steps in order") # --- OpenAI 4o+ / o1: json_schema with strict is strongest --- from langchain_openai import ChatOpenAI openai_llm = ChatOpenAI(model="gpt-4o-mini").with_structured_output( Recipe, method="json_schema", strict=True ) # --- Anthropic Claude: function_calling --- from langchain_anthropic import ChatAnthropic claude_llm = ChatAnthropic(model="claude-sonnet-5").with_structured_output( Recipe, method="function_calling" ) # --- Google Gemini: function_calling --- from langchain_google_genai import ChatGoogleGenerativeAI gemini_llm = ChatGoogleGenerativeAI(model="gemini-2.5-pro").with_structured_output( Recipe, method="function_calling" ) # --- Ollama / open-source: json_mode (weakest but broadest) --- from langchain_ollama import ChatOllama ollama_llm = ChatOllama(model="llama3.1:8b").with_structured_output( Recipe, method="json_mode" ) # All four accept the same invoke and return a Recipe object for name, llm in [("openai", openai_llm), ("claude", claude_llm), ("gemini", gemini_llm), ("ollama", ollama_llm)]: try: recipe = llm.invoke("How to make pancakes?") print(f"{name}: {recipe.name} — {len(recipe.ingredients)} ingredients") except Exception as e: print(f"{name}: ERROR {e}") # For debugging: include_raw returns both the parsed model AND the raw AIMessage structured_llm_debug = openai_llm.with_structured_output( Recipe, method="json_schema", include_raw=True ) result = structured_llm_debug.invoke("...") # result = {"raw": AIMessage(...), "parsed": Recipe(...), "parsing_error": None or Exception}
method="json_schema" with strict=True gives the strongest guarantee — the model literally cannot emit invalid JSON. For every other provider, function_calling is the sensible default.Flatten complex schemas for cross-provider compatibility
When the schema is complex, flatten it: replace nested Unions with a discriminated type field, cap nesting depth, and inline small nested models. Use include_raw=True to inspect parse failures.
# ❌ Too complex — deep nesting and Union # from typing import Union # class Address(BaseModel): ... # class Person(BaseModel): # name: str # contact: Union[EmailContact, PhoneContact, PostalContact] # history: List[Union[Employment, Education, Certification]] # ✓ Flatter — discriminated types from typing import List, Literal, Optional from pydantic import BaseModel, Field class ContactMethod(BaseModel): kind: Literal["email", "phone", "postal"] = Field(description="Contact type") value: str = Field(description="Contact value as a string") class HistoryItem(BaseModel): kind: Literal["employment", "education", "certification"] title: str = Field(description="Position, degree, or certification name") organization: str = Field(description="Company, school, or issuer") start_year: int end_year: int = Field(description="0 if ongoing") class Person(BaseModel): name: str = Field(description="Full name") contacts: List[ContactMethod] = Field(default_factory=list) history: List[HistoryItem] = Field(default_factory=list) # For debugging schemas, print the JSON schema import json print(json.dumps(Person.model_json_schema(), indent=2)) # Verify: no circular refs, no unions the provider rejects, all fields have descriptions # For validation failures, use include_raw structured_llm_debug = ChatOpenAI(model="gpt-4o-mini").with_structured_output( Person, method="json_schema", strict=True, include_raw=True ) result = structured_llm_debug.invoke("Some text about a person...") if result["parsing_error"]: print(f"Parse error: {result['parsing_error']}") print(f"Raw output was: {result['raw'].content}") else: person = result["parsed"]
kind field are portable, human-readable, and every strict provider accepts them. When in doubt, use this pattern instead of full Union types.Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Use Pydantic v2 exclusively; verify with
import pydantic; pydantic.VERSION. - Every field needs
Field(description="..."). Sparse descriptions cause bad extraction. - For OpenAI 4o+/o1:
method="json_schema"withstrict=True. - For other providers:
method="function_calling". - Avoid deep nested Union types — use discriminated unions with a Literal
kindfield. - For debugging, add
include_raw=Trueand inspect both parsed result and raw AIMessage. - Test structured output in CI against every provider you support.
Frequently asked questions
json_mode forces the output to be valid JSON but does not enforce a specific schema. json_schema enforces the exact schema you provide; the model literally cannot emit invalid data. Prefer json_schema when supported.method="json_mode" and JsonOutputParser you get progressive partial dicts. With function_calling or json_schema you typically get one final structured value. See our streaming page for details.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.