7 minute read

Mastering Structured Output with LangChain’s ChatOpenAI: A Comprehensive Guide to JSON Schema, Function Calling, and Response Validation

In modern AI application development, getting consistent, well-structured data from language models is crucial. While LLMs are powerful, their natural language outputs can be unpredictable and difficult to parse programmatically. LangChain’s ChatOpenAI integration offers robust solutions for obtaining structured data through various methods. This article explores how to leverage ChatOpenAI’s structured output features to reliably get JSON responses from your language models.

Understanding the Need for Structured Output

When building applications with LLMs, you often need responses in specific formats that can be easily processed by your application logic. For example, you might need:

  • User information in a consistent JSON structure
  • Database query parameters with specific fields
  • Structured analysis results that conform to a predefined schema

Without structured output controls, parsing free-form text responses can lead to brittle applications that break when the model’s output format changes slightly.

ChatOpenAI’s Structured Output Options

LangChain’s ChatOpenAI offers three primary methods for obtaining structured outputs:

  1. JSON Schema - Using OpenAI’s Structured Output API
  2. Function Calling - Using OpenAI’s tool-calling API (formerly function calling)
  3. JSON Mode - Using OpenAI’s JSON mode with prompt engineering

Let’s explore each approach, starting with the newest and most powerful option.

Method 1: JSON Schema (Structured Output API)

The JSON Schema method leverages OpenAI’s Structured Output API, which is supported by newer models like “gpt-4o-mini”, “gpt-4o-2024-08-06”, “o1”, and later models.

from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing import List, Optional

# Define your schema as a Pydantic model
class MovieReview(BaseModel):
    title: str = Field(description="The title of the movie")
    year: int = Field(description="The year the movie was released")
    rating: float = Field(description="Rating from 0-10")
    review_text: str = Field(description="The full text review")
    tags: List[str] = Field(description="Categories or genres that apply to this movie")

# Initialize the model
model = ChatOpenAI(model="gpt-4o-mini")

# Create a structured output chain
structured_model = model.with_structured_output(
    MovieReview,
    method="json_schema",  # This is now the default method
)

# Get a structured response
response = structured_model.invoke("Write a review for The Matrix movie from 1999")

# Now response is a MovieReview object
print(f"Movie: {response.title} ({response.year})")
print(f"Rating: {response.rating}/10")
print(f"Review: {response.review_text}")
print(f"Tags: {', '.join(response.tags)}")

This approach offers strong schema validation and guarantees that the output will match your defined structure.

Method 2: Function Calling (Tool Calling API)

Function calling uses OpenAI’s tool-calling API (formerly known as function calling) to guide the model to generate outputs matching a specific schema:

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage

# Define a function schema
function_schema = {
    "name": "extract_product_info",
    "description": "Extract product information from text",
    "parameters": {
        "type": "object",
        "properties": {
            "product_name": {"type": "string", "description": "The name of the product"},
            "price": {"type": "number", "description": "The price of the product in USD"},
            "features": {
                "type": "array", 
                "items": {"type": "string"},
                "description": "Key features of the product"
            },
            "in_stock": {"type": "boolean", "description": "Whether the product is in stock"}
        },
        "required": ["product_name", "price"]
    }
}

# Initialize the model with function calling capability
model = ChatOpenAI(model="gpt-3.5-turbo-0125")

# Create a message with function calling
response = model.invoke(
    [HumanMessage(content="Extract information from this text: The iPhone 14 Pro is available for $999 with features like Dynamic Island, Always-On display, and a 48MP camera. Currently out of stock.")],
    tools=[function_schema],
    tool_choice={"type": "function", "function": {"name": "extract_product_info"}}
)

print(response.tool_calls[0].function.arguments)

The output will be a JSON string containing the structured data:

{
  "product_name": "iPhone 14 Pro",
  "price": 999,
  "features": ["Dynamic Island", "Always-On display", "48MP camera"],
  "in_stock": false
}

Method 3: JSON Mode

JSON mode relies on instructing the model to format its responses as JSON:

from langchain_openai import ChatOpenAI
from typing import Dict

# Initialize the model with JSON mode
model = ChatOpenAI(
    model="gpt-3.5-turbo-0125",
    model_kwargs={"response_format": {"type": "json_object"}}
)

# Create a structured output chain
structured_model = model.with_structured_output(
    Dict,  # We can use a simple Dict type here
    method="json_mode",
    include_raw=True  # To see both raw and parsed outputs
)

# The prompt needs to include instructions about the desired JSON structure
response = structured_model.invoke(
    "Extract the following information in JSON format: name, age, and occupation of Mark Zuckerberg."
)

print("Parsed output:", response["parsed"])
print("\nRaw output:", response["raw"].content)

Advanced Features: Validation and Error Handling

Strict Mode

For applications requiring exact schema conformance, ChatOpenAI supports a strict parameter:

structured_model = model.with_structured_output(
    MovieReview,
    method="json_schema",
    strict=True  # Enforces exact schema matching
)

When strict=True, both the input schema and model output will be validated according to OpenAI’s supported schema constraints. This ensures the highest level of conformance but comes with some limitations on the types of schemas that can be used.

Error Handling with include_raw

To gracefully handle parsing errors, use the include_raw parameter:

structured_model = model.with_structured_output(
    MovieReview,
    method="json_schema",
    include_raw=True  # Returns both raw and parsed responses
)

response = structured_model.invoke("Write a review for Inception")

if response["parsing_error"]:
    print(f"Error parsing response: {response['parsing_error']}")
    # Fall back to using the raw response
    print(f"Raw response: {response['raw'].content}")
else:
    # Use the parsed structured data
    movie = response["parsed"]
    print(f"Successfully parsed: {movie.title}")

Streaming Structured Outputs

ChatOpenAI also supports streaming with structured outputs, though the complete structure is only available at the end of the stream:

from langchain_core.output_parsers import StrOutputParser

# Create a streaming structured output chain
streaming_model = model.with_structured_output(
    MovieReview,
    method="json_schema"
)

# Create a streaming chain
for chunk in streaming_model.stream("Write a review for Interstellar"):
    # During streaming, you'll get chunks of the raw response
    print(chunk, end="", flush=True)

# For a more complete streaming solution, you might need to use astream_events

Practical Example: Building a Product Catalog Extractor

Let’s put everything together with a practical example that extracts product information from text descriptions:

from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing import List, Optional

class Product(BaseModel):
    name: str = Field(description="The product name")
    description: str = Field(description="Brief product description")
    price: float = Field(description="Price in USD")
    category: str = Field(description="Product category")
    features: List[str] = Field(description="Key product features")
    availability: bool = Field(description="Whether the product is in stock")
    rating: Optional[float] = Field(description="Customer rating from 0-5 if available")

# Initialize the model
model = ChatOpenAI(model="gpt-4o-mini")

# Create a structured output extractor
product_extractor = model.with_structured_output(
    Product,
    method="json_schema",
    strict=True
)

# Sample product descriptions
product_texts = [
    """
    The Sony WH-1000XM4 wireless noise-canceling headphones offer exceptional sound quality 
    with industry-leading noise cancellation. Features include 30-hour battery life, touch 
    controls, and speak-to-chat technology. Available now for $349.99. Rated 4.8/5 by customers.
    """,
    """
    Introducing the EcoBlend Blender, perfect for smoothies and food prep. 1000W motor with 
    5 speed settings and pulse function. Dishwasher-safe parts and BPA-free materials.
    Currently out of stock, regular price $89.99.
    """
]

# Process each product description
for text in product_texts:
    product = product_extractor.invoke(f"Extract product information from this text: {text}")
    
    print(f"\nProduct: {product.name}")
    print(f"Category: {product.category}")
    print(f"Price: ${product.price}")
    print(f"Description: {product.description}")
    print(f"Features: {', '.join(product.features)}")
    print(f"In Stock: {'Yes' if product.availability else 'No'}")
    if product.rating is not None:
        print(f"Rating: {product.rating}/5")

Comparing the Three Methods

Let’s summarize the key differences between the three structured output methods:

Method Supported Models Pros Cons
JSON Schema gpt-4o-mini, gpt-4o-2024-08-06, o1, and later Most reliable schema conformance, best validation Limited to newer models
Function Calling Most GPT-3.5 and GPT-4 models Wide model support, good schema adherence Less strict validation than JSON Schema
JSON Mode Models supporting response_format Works with many models Requires careful prompt engineering, less reliable

Best Practices

  1. Choose the right method for your model: Use JSON Schema for newer models, Function Calling for older models that don’t support JSON Schema, and JSON Mode as a fallback.

  2. Use Pydantic for complex schemas: Pydantic models provide clear validation and type checking for your structured outputs.

  3. Handle parsing errors gracefully: Always implement error handling, especially for critical applications.

  4. Consider streaming for long responses: For lengthy structured outputs, streaming can improve user experience.

  5. Test with diverse inputs: Validate your structured output setup with a variety of inputs to ensure robustness.

  6. Use strict mode when accuracy is critical: When you absolutely need exact schema conformance, enable strict mode.

Conclusion

LangChain’s ChatOpenAI integration provides powerful tools for obtaining structured, reliable JSON responses from language models. By leveraging JSON Schema, Function Calling, or JSON Mode, you can ensure your applications receive data in a consistent, parseable format.

The choice between these methods depends on your specific requirements, the models you’re using, and the level of schema validation you need. For the most robust applications, consider implementing fallback mechanisms that try multiple approaches when one fails.

By mastering these structured output techniques, you’ll build more reliable AI applications that can confidently integrate language model outputs into your business logic and data pipelines.

This post was originally written in my native language and then translated using an LLM. I apologize if there are any grammatical inconsistencies.