Module 1: Models and Providers

Structured Output

Capsule overview

So far, every time you invoke a model you get free-form text. The model answers with a string of characters that you, as the developer, have to interpret by hand. If you want to pull a title, a rating and a summary out of a review, you'd have to parse the text with regular expressions, split on delimiters, or ask the model to "please answer in JSON" — and pray that it does.

Structured Output removes that problem entirely. Instead of getting text and parsing it, you tell the model exactly which structure you want — with types, fields and descriptions — and you get back a typed Python object. No regex, no brittle parsing, no surprises. You define a schema with Pydantic, TypedDict or JSON Schema, and the with_structured_output method makes sure the model responds in exactly that format.

This capability is fundamental for any serious application. APIs that return data, pipelines that process information, agents that make decisions — all of them need structured data, not free-form text. In this module's project, you'll use Structured Output to extract metadata from every chat response: which provider was used, how many tokens it consumed, and the latency of the call.


Why Structured Output?

The problem: parsing free-form text is brittle

Imagine you ask a model to analyze a movie:

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model

model = init_chat_model("openai:gpt-4.1-mini")

response = model.invoke("Analyze the movie Inception. Give me a title, a rating from 1 to 10, and a short summary.")
print(response.content)
# Possible output 1: "Title: Inception\nRating: 9/10\nSummary: A thief who steals secrets..."
# Possible output 2: "**Inception** - 9 stars. A thriller about dreams within dreams..."
# Possible output 3: "The movie Inception, directed by Christopher Nolan, deserves a 9..."

Three runs, three different formats. Now try extracting the rating programmatically. You'd need regex, heuristics, and it would still break on unexpected formats.

The solution: typed responses

With Structured Output, you define the structure once and the model always respects it:

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field

class MovieReview(BaseModel):
    title: str = Field(description="The movie's title")
    rating: int = Field(description="Rating from 1 to 10")
    summary: str = Field(description="A one-sentence summary")

model = init_chat_model("openai:gpt-4.1-mini")
structured_model = model.with_structured_output(MovieReview)

result = structured_model.invoke("Analyze the movie Inception")
print(type(result))     # <class 'MovieReview'>
print(result.title)     # Inception
print(result.rating)    # 9
print(result.summary)   # A thief skilled at extracting secrets through dreams...

The result is no longer free-form text — it's a MovieReview object with typed attributes. result.rating is always an int, not a string you have to parse.

Concrete benefits

  • Type safety — Every field has a defined type. No manual conversions
  • Parse reliability — The model respects the structure 100% of the time
  • No regex — Never write a regular expression to extract data again
  • Automatic validation — Pydantic validates types and constraints for you
  • IDE autocomplete — Your editor knows which fields exist
  • Composability — Structured objects plug straight into the rest of your code

with_structured_output with Pydantic

Pydantic is the most recommended approach. It gives you type validation, field descriptions that guide the model, and native Python objects as the result.

Basic example

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field

class Sentiment(BaseModel):
    label: str = Field(description="Sentiment: positive, negative or neutral")
    confidence: float = Field(description="Confidence level between 0.0 and 1.0")
    explanation: str = Field(description="A brief explanation of the analysis")

model = init_chat_model("openai:gpt-4.1-mini")
analyzer = model.with_structured_output(Sentiment)

result = analyzer.invoke("I love this product, it's incredible")
print(result.label)        # positive
print(result.confidence)   # 0.95
print(result.explanation)  # The text expresses enthusiasm and clear satisfaction...

How it works

  1. You define a Pydantic model with typed fields and descriptions
  2. You call model.with_structured_output(YourModel) — this returns a new model that always produces objects of the type you specified
  3. You invoke the structured model normally with .invoke()
  4. The result is an instance of your Pydantic model, not text

Field(description=...) matters: those descriptions tell the model what you expect in each field. Think of them as mini-instructions for the LLM.

Supported field types

TypeExampleDescription
strname: strFree text
intcount: intWhole number
floatscore: floatDecimal number
boolis_valid: boolTrue or false
list[str]tags: list[str]A list of strings
Optional[str]note: Optional[str]Can be None
Enumstatus: MyEnumA value from a closed set
Literal["a", "b"]choice: Literal["a", "b"]A restricted literal value

Extracting multiple items

A very common pattern is extracting a list of objects from a text:

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field

class Person(BaseModel):
    name: str = Field(description="The person's full name")
    role: str = Field(description="The role or relationship mentioned")

class PeopleExtraction(BaseModel):
    people: list[Person] = Field(description="List of people mentioned in the text")
    total_count: int = Field(description="Total number of people found")

model = init_chat_model("openai:gpt-4.1-mini")
extractor = model.with_structured_output(PeopleExtraction)

text = """
The meeting was attended by Carlos (CTO), Ana María (product manager) 
and the new developer Luis García.
"""

result = extractor.invoke(f"Extract the people mentioned: {text}")
print(result.total_count)   # 3
for person in result.people:
    print(f"  - {person.name}: {person.role}")
# Output:
#   - Carlos: CTO
#   - Ana María: Product Manager
#   - Luis García: Developer

with_structured_output with TypedDict

If you don't need the validation Pydantic offers and you'd rather work with dictionaries, you can use TypedDict. The result will be a Python dict instead of a Pydantic object.

Basic example

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from typing import TypedDict, Annotated

class MovieReview(TypedDict):
    title: Annotated[str, "The movie's title"]
    rating: Annotated[int, "Rating from 1 to 10"]
    summary: Annotated[str, "A one-sentence summary"]

model = init_chat_model("openai:gpt-4.1-mini")
structured_model = model.with_structured_output(MovieReview)

result = structured_model.invoke("Analyze the movie The Matrix")
print(type(result))       # <class 'dict'>
print(result["title"])    # The Matrix
print(result["rating"])   # 9
print(result["summary"])  # A programmer discovers reality is a simulation...

The key difference from Pydantic

With TypedDict the result is a dictionary (result["title"]), not an object with attributes (result.title). There's no automatic type validation — if the model returns a string where you expected an int, no error gets raised.

When do you use TypedDict?

  • ✅ When the output goes straight into a JSON response (API endpoints)
  • ✅ When you don't need strict validation
  • ✅ When you'd rather work with dictionaries out of familiarity
  • ❌ When you need constraint validation (ranges, patterns, etc.)
  • ❌ When you want custom methods on the result

with_structured_output with JSON Schema

The third approach uses a JSON Schema directly. This is useful when the schema is generated dynamically — for example, when the user defines the extraction structure at runtime.

Basic example

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model

json_schema = {
    "title": "MovieReview",
    "description": "Analysis of a movie",
    "type": "object",
    "properties": {
        "title": {
            "type": "string",
            "description": "The movie's title"
        },
        "rating": {
            "type": "integer",
            "description": "Rating from 1 to 10"
        },
        "summary": {
            "type": "string",
            "description": "A one-sentence summary"
        }
    },
    "required": ["title", "rating", "summary"]
}

model = init_chat_model("openai:gpt-4.1-mini")
structured_model = model.with_structured_output(json_schema)

result = structured_model.invoke("Analyze the movie Interstellar")
print(type(result))       # <class 'dict'>
print(result["title"])    # Interstellar
print(result["rating"])   # 9
print(result["summary"])  # A group of explorers travels through a wormhole...

When do you use JSON Schema?

  • ✅ Schemas generated at runtime (dynamic forms, per-user configuration)
  • ✅ When you receive the schema from an external source (an API, a database)
  • ✅ When you need compatibility with systems that already use JSON Schema
  • ❌ When the schema is fixed — Pydantic is more ergonomic and safer

Nested structures

Simple schemas cover a lot of cases, but real applications need hierarchical structures. Pydantic lets you nest models inside models.

Example: a report with sections

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field

class Section(BaseModel):
    title: str = Field(description="The section's title")
    content: str = Field(description="The section's content, in 2-3 sentences")

class Report(BaseModel):
    topic: str = Field(description="The report's topic")
    executive_summary: str = Field(description="A one-sentence executive summary")
    sections: list[Section] = Field(description="3 sections of the report")
    conclusion: str = Field(description="A one-sentence conclusion")

model = init_chat_model("openai:gpt-4.1-mini")
reporter = model.with_structured_output(Report)

result = reporter.invoke("Write a brief report on the impact of AI in education")
print(f"Topic: {result.topic}")
print(f"Summary: {result.executive_summary}")
print(f"Sections: {len(result.sections)}")
for section in result.sections:
    print(f"  - {section.title}: {section.content[:80]}...")
print(f"Conclusion: {result.conclusion}")
# Output:
# Topic: The Impact of Artificial Intelligence on Education
# Summary: AI is transforming education through personalization...
# Sections: 3
#   - Personalized learning: AI systems can adapt...
#   - Administrative automation: Tasks like grading and reporting...
#   - Challenges and ethical considerations: The digital divide and privacy...
# Conclusion: AI has the potential to democratize education...

Nesting can go several levels deep, but keep the complexity reasonable. Schemas more than 3 levels deep tend to produce less reliable results.


include_raw: getting the original response

By default, with_structured_output returns only the parsed object. But sometimes you need access to the model's original response — for debugging, logging, or to pull metadata like tokens used.

Turning on include_raw

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field

class Sentiment(BaseModel):
    label: str = Field(description="Sentiment: positive, negative or neutral")
    confidence: float = Field(description="Confidence between 0.0 and 1.0")

model = init_chat_model("openai:gpt-4.1-mini")
analyzer = model.with_structured_output(Sentiment, include_raw=True)

result = analyzer.invoke("This restaurant has the worst food I have ever tried")
print(type(result))   # <class 'dict'>
print(result.keys())  # dict_keys(['raw', 'parsed', 'parsing_error'])

With include_raw=True, the result changes: instead of getting the Pydantic object directly, you get a dictionary with three keys:

KeyTypeContents
rawAIMessageThe model's original response, with complete metadata
parsedYour Pydantic model (or None)The parsed object, same as without include_raw
parsing_errorException or NoneThe parsing error, if there was one

Using include_raw for debugging

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field

class Sentiment(BaseModel):
    label: str = Field(description="Sentiment: positive, negative or neutral")
    confidence: float = Field(description="Confidence between 0.0 and 1.0")

model = init_chat_model("openai:gpt-4.1-mini")
analyzer = model.with_structured_output(Sentiment, include_raw=True)

result = analyzer.invoke("This restaurant has the worst food I have ever tried")

if result["parsing_error"]:
    print(f"Error: {result['parsing_error']}")
    print(f"Raw: {result['raw'].content}")
else:
    parsed = result["parsed"]
    raw = result["raw"]
    print(f"Sentiment: {parsed.label} (confidence: {parsed.confidence})")
    print(f"Token usage: {raw.usage_metadata}")
# Output:
# Sentiment: negative (confidence: 0.95)
# Token usage: {'input_tokens': 45, 'output_tokens': 12, 'total_tokens': 57}

When do you use include_raw?

  • ✅ Debugging during development — seeing what the model actually answered
  • ✅ Logging in production — storing raw responses for auditing
  • ✅ Token tracking — pulling usage_metadata to monitor costs
  • ✅ Error handling — catching and handling parsing errors without crashing your app

Comparison: Pydantic vs TypedDict vs JSON Schema

FeaturePydanticTypedDictJSON Schema
Result typeObject with attributes (result.field)Dictionary (result["field"])Dictionary (result["field"])
Type validation✅ Automatic❌ No❌ No
Field descriptionsField(description=...)Annotated[type, "desc"]"description": "..."
Nested structures✅ Natural⚠️ Limited✅ With $ref
Constraints (min, max, pattern)Field(ge=1, le=10)❌ No"minimum": 1
IDE autocomplete✅ Full⚠️ Partial❌ No
Dynamic schemas❌ Static❌ Static✅ Can be generated at runtime
Learning curveMediumLowHigh
RecommendationUse by defaultQuick prototypesDynamic schemas

Rule: Use Pydantic unless you have a specific reason not to. TypedDict for quick prototypes where you don't want to install Pydantic (though it already ships with LangChain). JSON Schema only when the schema is generated dynamically.


Connection to the project

In the Multi-Provider Chat with Fallback, you'll use Structured Output for two things:

  1. Response metadata: Every chat response will include structured metadata — which provider generated it, how long it took, and how many tokens it used. This lets you monitor the system without parsing logs by hand.

  2. Formatted responses: When the user asks for analysis or data extraction, the chat will use Structured Output to return consistent results regardless of which provider generated them.

The metadata schema will look like this:

from pydantic import BaseModel, Field

class ResponseMetadata(BaseModel):
    provider: str = Field(description="The provider that generated the response")
    model_name: str = Field(description="The name of the model used")
    latency_ms: float = Field(description="Latency in milliseconds")
    tokens_used: int = Field(description="Total tokens consumed")

This pattern — combining include_raw to pull metadata out of the AIMessage with Pydantic for the structured response — is exactly what you'll implement in the project capsule.


Troubleshooting

Problem 1: The model doesn't respect the structure

Symptom: You get None or a parsing error instead of the expected object. Cause: The prompt is ambiguous or contradicts the schema's structure. Fix: Make sure the prompt is compatible with the schema. The descriptions in Field() help the model understand what to generate.

# Bad — the prompt asks for free-form output, but the schema expects structure
result = structured_model.invoke("Write whatever you want about AI")

# Good — the prompt is compatible with the structure
result = structured_model.invoke("Analyze the impact of AI on education")

Problem 2: Pydantic raises ValidationError

Symptom: pydantic.ValidationError: 1 validation error for MyModel. Cause: The model generated a value that doesn't pass Pydantic's validation (for example, a string where you expected an int). Fix: Use include_raw=True to inspect what the model actually generated, and tighten up your field descriptions so they're more explicit.

analyzer = model.with_structured_output(MyModel, include_raw=True)
result = analyzer.invoke("...")
if result["parsing_error"]:
    print(f"Error: {result['parsing_error']}")
    print(f"Raw: {result['raw'].content}")

Problem 3: with_structured_output isn't available

Symptom: AttributeError: 'ChatModel' object has no attribute 'with_structured_output'. Cause: An old LangChain version, or a provider that doesn't support structured output. Fix:

pip install --upgrade langchain langchain-core langchain-openai

Check that you're on LangChain v1.2+. The main providers (OpenAI, Anthropic, Google) support structured output.

Problem 4: Optional fields always come back as None

Symptom: The optional fields never get filled even though the information is in the text. Cause: The descriptions of the optional fields aren't clear enough. Fix: Improve the descriptions and spell out when the field should be filled:

# Bad
assignee: Optional[str] = Field(description="Assigned person")

# Good
assignee: Optional[str] = Field(
    description="Name of the person assigned to the task. None only if nobody is mentioned."
)

Exercises

Exercise 1: Contact extraction (Easy)

Define a Pydantic model ContactInfo with fields for name, email and phone (the last two optional). Use with_structured_output to extract contact information from natural-language text.

Test text: "Hi, I'm Laura Martínez. My email is laura@example.com and you can call me at 555-1234."

See solution
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
from typing import Optional

class ContactInfo(BaseModel):
    name: str = Field(description="The person's full name")
    email: Optional[str] = Field(description="Email address, if mentioned")
    phone: Optional[str] = Field(description="Phone number, if mentioned")

model = init_chat_model("openai:gpt-4.1-mini")
extractor = model.with_structured_output(ContactInfo)

text = "Hi, I'm Laura Martínez. My email is laura@example.com and you can call me at 555-1234."
result = extractor.invoke(f"Extract the contact information: {text}")

print(f"Name: {result.name}")     # Laura Martínez
print(f"Email: {result.email}")   # laura@example.com
print(f"Phone: {result.phone}")   # 555-1234

Explanation: The model automatically identifies each piece of information and assigns it to the right field. The Optional fields get filled when the information is present and stay None when it isn't.

Exercise 2: Classification with an Enum (Easy)

Build a support-ticket classifier that sorts each ticket into one of these categories: bug, feature_request, question, complaint. Use an Enum to restrict the possible values. Also include an urgency field (1-5) and a summary.

See solution
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
from enum import Enum

class TicketCategory(str, Enum):
    BUG = "bug"
    FEATURE_REQUEST = "feature_request"
    QUESTION = "question"
    COMPLAINT = "complaint"

class TicketClassification(BaseModel):
    category: TicketCategory = Field(description="The ticket's category")
    urgency: int = Field(description="Urgency level from 1 (low) to 5 (critical)")
    summary: str = Field(description="A one-sentence summary of the ticket")

model = init_chat_model("openai:gpt-4.1-mini")
classifier = model.with_structured_output(TicketClassification)

tickets = [
    "The checkout page crashes when I use Safari. I can't complete my purchase.",
    "Could you add dark mode? It would be great for working at night.",
    "How do I export my data to CSV?",
]

for ticket in tickets:
    result = classifier.invoke(f"Classify this support ticket: {ticket}")
    print(f"Category: {result.category.value} | Urgency: {result.urgency} | {result.summary}")
# Output:
# Category: bug | Urgency: 4 | Checkout page failure on Safari...
# Category: feature_request | Urgency: 2 | Request for dark mode...
# Category: question | Urgency: 1 | Question about exporting data to CSV...

Explanation: The Enum guarantees the category will always be one of the 4 valid values. The model can't invent new categories. Combined with the int-typed urgency field, you get data that's ready to process programmatically.

Exercise 3: Nested structure with TypedDict (Medium)

Recreate the Report-with-sections example, but this time using TypedDict instead of Pydantic. Compare the development experience of both approaches.

See solution
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from typing import TypedDict, Annotated

class Section(TypedDict):
    title: Annotated[str, "The section's title"]
    content: Annotated[str, "The section's content, in 2-3 sentences"]

class Report(TypedDict):
    topic: Annotated[str, "The report's topic"]
    executive_summary: Annotated[str, "A one-sentence executive summary"]
    sections: Annotated[list[Section], "3 sections of the report"]
    conclusion: Annotated[str, "A one-sentence conclusion"]

model = init_chat_model("openai:gpt-4.1-mini")
reporter = model.with_structured_output(Report)

result = reporter.invoke("Write a brief report on the impact of AI in healthcare")
print(f"Topic: {result['topic']}")
print(f"Summary: {result['executive_summary']}")
for section in result["sections"]:
    print(f"  - {section['title']}: {section['content'][:60]}...")
print(f"Conclusion: {result['conclusion']}")

Explanation: With TypedDict you reach fields via result["key"] instead of result.key. The structure works the same, but you lose automatic validation and IDE autocomplete. For nested schemas, Pydantic is usually more ergonomic.

Exercise 4: Dynamic schema with JSON Schema (Medium)

Write a function extract_from_text(text, fields) that takes a text and a dictionary of fields with their descriptions, generates a JSON Schema dynamically, and extracts the information. Try it with at least two different sets of fields over the same text.

See solution
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model

def extract_from_text(text: str, fields: dict[str, str]) -> dict:
    """Extracts information from a text using a dynamically generated schema."""
    schema = {
        "title": "Extraction",
        "type": "object",
        "properties": {
            name: {"type": "string", "description": desc}
            for name, desc in fields.items()
        },
        "required": list(fields.keys())
    }
    
    model = init_chat_model("openai:gpt-4.1-mini")
    extractor = model.with_structured_output(schema)
    return extractor.invoke(f"Extract the following information from the text: {text}")

article = """
Apple unveiled the iPhone 16 in September 2024 with a base price of $799.
CEO Tim Cook highlighted the new AI capabilities built into the device.
Apple's stock rose 2% after the announcement.
"""

business_fields = {
    "company": "The company's name",
    "product": "The product unveiled",
    "price": "The price mentioned",
}
print("Business extraction:", extract_from_text(article, business_fields))
# Output: {'company': 'Apple', 'product': 'iPhone 16', 'price': '$799'}

market_fields = {
    "stock_movement": "The stock movement mentioned",
    "executive": "The name of the executive mentioned",
    "date": "The date of the event",
}
print("Market extraction:", extract_from_text(article, market_fields))
# Output: {'stock_movement': 'Rose 2%', 'executive': 'Tim Cook', 'date': 'September 2024'}

Explanation: The same function extracts completely different information from the same text, depending on the fields you hand it. This pattern is powerful for applications where users define what they want to extract.

Exercise 5: include_raw for monitoring (Advanced)

Write a function analyze_with_metrics that uses include_raw=True to return both the structured analysis and usage metrics (tokens, model). If there's a parsing error, it should return the error and the raw response instead of crashing.

See solution
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field

class Analysis(BaseModel):
    topic: str = Field(description="The topic analyzed")
    sentiment: str = Field(description="Overall sentiment: positive, negative or neutral")
    key_points: list[str] = Field(description="3 key points from the analysis")

def analyze_with_metrics(text: str) -> dict:
    """Analyzes text and returns the result together with usage metrics."""
    model = init_chat_model("openai:gpt-4.1-mini")
    analyzer = model.with_structured_output(Analysis, include_raw=True)
    
    result = analyzer.invoke(f"Analyze the following text: {text}")
    
    raw = result["raw"]
    parsed = result["parsed"]
    error = result["parsing_error"]
    
    metrics = {
        "tokens": raw.usage_metadata if hasattr(raw, "usage_metadata") else None,
        "model": raw.response_metadata.get("model_name", "unknown"),
    }
    
    if error:
        return {
            "success": False,
            "error": str(error),
            "raw_content": raw.content,
            "metrics": metrics,
        }
    
    return {
        "success": True,
        "analysis": {
            "topic": parsed.topic,
            "sentiment": parsed.sentiment,
            "key_points": parsed.key_points,
        },
        "metrics": metrics,
    }

result = analyze_with_metrics(
    "The new Python framework has gained popularity fast. "
    "Developers are adopting it for its simplicity and performance."
)

if result["success"]:
    print(f"Topic: {result['analysis']['topic']}")
    print(f"Sentiment: {result['analysis']['sentiment']}")
    for point in result["analysis"]["key_points"]:
        print(f"  - {point}")
    print(f"Tokens: {result['metrics']['tokens']}")
    print(f"Model: {result['metrics']['model']}")
else:
    print(f"Error: {result['error']}")
    print(f"Raw: {result['raw_content']}")
# Output:
# Topic: A new Python framework
# Sentiment: positive
#   - Gained popularity fast
#   - Developers are adopting it
#   - Praised for simplicity and performance
# Tokens: {'input_tokens': 85, 'output_tokens': 45, 'total_tokens': 130}
# Model: gpt-4.1-mini

Explanation: include_raw=True gives you access to the model's complete response, including token metadata. The pattern of checking parsing_error before touching parsed makes your code robust against parsing errors without reaching for try/except.


Summary

In this capsule you learned:

  • Structured Output removes manual parsing of free-form text — you define a schema and get typed objects back
  • Pydantic is the recommended approach: it gives you validation, field descriptions, and objects with attributes
  • TypedDict is the lightweight alternative when you'd rather have dictionaries and don't need validation
  • JSON Schema is for dynamic schemas generated at runtime
  • Nested structures let you model hierarchical data with Pydantic models inside models
  • include_raw=True gives you access to the original response for debugging, logging, and token tracking
  • Field descriptions are instructions for the model — write them clearly

Next capsule: Multimodal and Reasoning — how to process images, audio and video with models, and how to make the model show its reasoning steps.


Additional resources

  1. Structured Output — LangChain Docs — The official conceptual guide to structured output
  2. How to return structured data from a model — A step-by-step tutorial with examples
  3. Pydantic v2 Documentation — Complete Pydantic reference for advanced schemas
  4. OpenAI Structured Outputs — OpenAI's implementation of the standard
  5. Anthropic Tool Use for Structured Output — How Anthropic implements structured output via tools
  6. JSON Schema Specification — Reference for writing JSON Schemas by hand
  7. TypedDict — Python Docs — Official TypedDict documentation
  8. LangChain Chat Models API Reference — Reference for the with_structured_output method

Module 1 — LangChain & LangGraph: From Chains to Agents