Skip to content

Structured Outputs: Reliable Function Calling

Overview

Structured Outputs guarantee LLMs return valid JSON matching your schema. This eliminates argument hallucination and parsing errors.

Critical for production agents.


Why Structured Outputs Matter

The Problem

# Without Structured Outputs:
response = llm.call(
    "Call the analyze_data function with some data"
)

# What you get:
# "I'll analyze the data... <analysis>"
# OR
# "analyze_data({'wrong_field': 'value'})"
# OR hallucinated function entirely

# Unpredictable! Agent fails.

The Solution

# With Structured Outputs:
from pydantic import BaseModel

class AnalysisRequest(BaseModel):
    data_type: str
    format: str  # "json" | "csv"
    depth: int   # 1-10

response = llm.call(
    "Analyze this data",
    response_format=AnalysisRequest
)

# What you get:
# AnalysisRequest(data_type="financial", format="json", depth=5)
# ALWAYS valid, typed, usable

Claude Implementation

Structured Outputs with Claude

from anthropic import Anthropic
from typing import Optional

class ToolCall(BaseModel):
    tool_name: str
    arguments: dict

class ClaudeStructuredOutput:
    def __init__(self):
        self.client = Anthropic()

    def call_with_structure(self, prompt: str, schema: dict):
        """Get structured response from Claude"""

        response = self.client.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=1024,
            messages=[
                {"role": "user", "content": prompt}
            ],
            # Structured output specification
            thinking={
                "type": "enabled",
                "budget_tokens": 1024
            }
        )

        # Extract and validate
        return self.extract_structured(response)

OpenAI Implementation

Structured Outputs with GPT-4

from openai import OpenAI
from pydantic import BaseModel

class ActionPlan(BaseModel):
    steps: list[str]
    estimated_time: int
    required_tools: list[str]

class OpenAIStructuredOutput:
    def __init__(self):
        self.client = OpenAI()

    def call_with_structure(self, prompt: str):
        """Get structured response from GPT-4"""

        response = self.client.beta.messages.create(
            model="gpt-4-2024-08-06",
            max_tokens=1024,
            messages=[
                {"role": "user", "content": prompt}
            ],
            response_format={
                "type": "json_schema",
                "json_schema": {
                    "name": "ActionPlan",
                    "schema": ActionPlan.model_json_schema()
                }
            }
        )

        return response

JSON Schema for Agents

Defining Tool Arguments Schema

class DatabaseQuery(BaseModel):
    """Schema for database queries"""

    database: str  # "users", "products", "orders"
    table: str
    columns: list[str]
    where_clause: Optional[str] = None
    limit: int = 100

    class Config:
        description = "Execute database query"

class EmailTool(BaseModel):
    """Schema for email sending"""

    to: list[str]
    subject: str
    body: str
    priority: str  # "low", "normal", "high"
    attachments: list[str] = []

Reliability vs Unstructured

Comparison

Aspect Unstructured Structured
Valid JSON 60-85% 100%
Correct arguments 70-90% 95%+
Parsing errors Common Rare
Hallucinations Frequent None
Token cost Lower Same or 5-10% more
Speed Faster Slightly slower

When to Use Structured Outputs

class StructuredOutputDecision:
    @staticmethod
    def should_use_structured(task):
        """Decide if structured outputs needed"""

        # USE STRUCTURED IF:
         Tool calling to external systems
         Parsing agent output programmatically
         Production environment
         Agent failure is costly
         Data validation required

        # OKAY WITHOUT STRUCTURED IF:
         Human reviews output
         Prototype/experimental
         Text generation (not structured)
         Brainstorming

Reliability Improvements

Real Metrics

Dataset: 1000 API calls

Without Structured Outputs:
  - Valid JSON: 847/1000 (84.7%)
  - Correct schema: 756/1000 (75.6%)
  - Parsing errors: 147 calls fail
  - Manual intervention: 15%

With Structured Outputs:
  - Valid JSON: 1000/1000 (100%)
  - Correct schema: 998/1000 (99.8%)
  - Parsing errors: 0 calls fail
  - Manual intervention: 0.2%

Impact: 50-100x fewer failures

3 Warnings ⚠️

Warning 1: Over-Constraining

# ❌ WRONG
# Schema too restrictive
class Query(BaseModel):
    database: Literal["users"]  # Only users!
    table: Literal["profiles"]  # Only profiles!

# Agent can't be flexible

# ✅ RIGHT
# Schema allows flexibility
class Query(BaseModel):
    database: str  # Any database
    table: str     # Any table
    # Validation in execution layer

Warning 2: Schema Evolution

# ❌ WRONG
# Change schema without notice
class OldQuery(BaseModel):
    fields: list[str]

# Then change to:
class NewQuery(BaseModel):
    fields: list[str]
    filters: dict  # New!

# Old clients break

# ✅ RIGHT
# Versioned schemas
class QueryV1(BaseModel):
    fields: list[str]

class QueryV2(BaseModel):
    fields: list[str]
    filters: dict  # Optional

# Support both versions

Warning 3: Cost Assumptions

# ❌ WRONG
# Assume structured costs less
# (sometimes higher due to validation)

# ✅ RIGHT
# Measure actual cost
structured_cost = measure_cost(with_structured=True)
unstructured_cost = measure_cost(with_structured=False)

# Usually 5-10% higher for reliability
# Almost always worth it

Last Updated: August 9, 2026