AI Structured Output -- JSON Mode and Function Calling

Why Structured Output Is Necessary

LLMs generate free-form text, but real applications need data in a defined format. API responses must be JSON, and database storage requires exact fields.

Using a restaurant analogy, “give me something delicious” (free text) versus “please fill out this order form” (structured output) — the latter gets you a more accurate result.

Common situations requiring structured output:

SituationRequired FormatExample
API response generationJSON{"name": "John", "age": 30}
Data extractionSchema-based objectExtracting schedule info from emails
Function callingFunction argumentssearch(query="weather", city="Seoul")
Classification tasksEnum"sentiment": "positive"
Workflow controlNext step decision{"action": "search", "params": {...}}

Method 1: JSON Mode

Major APIs including OpenAI and Anthropic support JSON mode. It guarantees the response is always valid JSON.

from openai import OpenAI

client = OpenAI()

# Enable JSON mode
response = client.chat.completions.create(
    model="gpt-4o",
    response_format={"type": "json_object"},  # Enable JSON mode
    messages=[
        {
            "role": "system",
            "content": "Analyze the user's request and respond in JSON."
        },
        {
            "role": "user",
            "content": "Create a profile for John Smith, age 30, living in Gangnam, Seoul"
        }
    ]
)

import json
data = json.loads(response.choices[0].message.content)
print(json.dumps(data, ensure_ascii=False, indent=2))
# Output:
# {
#   "name": "John Smith",
#   "age": 30,
#   "address": {
#     "city": "Seoul",
#     "district": "Gangnam"
#   }
# }

Limitations of JSON mode: It guarantees valid JSON, but does not guarantee schema compliance. The age field could be a string or a number.

Method 2: Structured Outputs (Schema Enforcement)

OpenAI’s Structured Outputs generates responses that 100% comply with the schema when you define a JSON Schema.

from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()

# Define output schema with Pydantic
class UserProfile(BaseModel):
    name: str
    age: int
    email: str
    skills: list[str]
    is_active: bool

# Structured Outputs -- schema enforcement
response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "Generate a user profile."},
        {"role": "user", "content": "Jane Lee, age 25, who works with Python and JavaScript"}
    ],
    response_format=UserProfile  # Specify schema with Pydantic model
)

# Returns a type-guaranteed object
user = response.choices[0].message.parsed
print(f"Name: {user.name}")        # Output: Name: Jane Lee
print(f"Age: {user.age}")          # Output: Age: 25 (guaranteed int)
print(f"Skills: {user.skills}")    # Output: Skills: ['Python', 'JavaScript']
print(f"Active: {user.is_active}") # Output: Active: True (guaranteed bool)

Complex Schema Example

from pydantic import BaseModel, Field
from enum import Enum

class Sentiment(str, Enum):
    positive = "positive"
    negative = "negative"
    neutral = "neutral"

class Entity(BaseModel):
    name: str = Field(description="Entity name")
    type: str = Field(description="Entity type (person, place, organization, etc.)")

class TextAnalysis(BaseModel):
    """Text analysis result schema"""
    summary: str = Field(description="Text summary (1-2 sentences)")
    sentiment: Sentiment = Field(description="Sentiment analysis result")
    entities: list[Entity] = Field(description="List of extracted entities")
    keywords: list[str] = Field(description="Key keywords (max 5)")
    confidence: float = Field(description="Analysis confidence (0.0-1.0)")

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "Analyze the text."},
        {"role": "user", "content": "Samsung Electronics announced a new AI semiconductor at an event in Gangnam, Seoul. Market response is very positive."}
    ],
    response_format=TextAnalysis
)

analysis = response.choices[0].message.parsed
print(f"Sentiment: {analysis.sentiment}")     # Output: Sentiment: positive
print(f"Entities: {analysis.entities}")        # Output: [Entity(name='Samsung Electronics', type='organization'), ...]
print(f"Confidence: {analysis.confidence}")    # Output: Confidence: 0.92

Method 3: Function Calling (Tool Use)

Function Calling is a mechanism where the LLM decides which function to call and what arguments to pass. It’s the core of the agent pattern.

from openai import OpenAI
import json

client = OpenAI()

# Define tools (functions)
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Looks up the current weather for a specific city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "City name (e.g., Seoul, Busan)"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "Temperature unit"
                    }
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_restaurants",
            "description": "Searches for nearby restaurants",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {"type": "string", "description": "Search location"},
                    "cuisine": {"type": "string", "description": "Type of cuisine"},
                    "max_results": {"type": "integer", "description": "Maximum number of results"}
                },
                "required": ["location"]
            }
        }
    }
]

# LLM selects the appropriate function
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "user", "content": "What's the weather in Seoul? Also recommend Italian restaurants near Gangnam"}
    ],
    tools=tools,
    tool_choice="auto"  # LLM automatically selects tools
)

# Process function call results
for tool_call in response.choices[0].message.tool_calls:
    func_name = tool_call.function.name
    func_args = json.loads(tool_call.function.arguments)
    print(f"Function called: {func_name}")
    print(f"Arguments: {func_args}")
# Output:
# Function called: get_weather
# Arguments: {"city": "Seoul"}
# Function called: search_restaurants
# Arguments: {"location": "Gangnam", "cuisine": "Italian", "max_results": 5}

Anthropic Claude’s Tool Use

The Claude API provides similar tool use capabilities.

import anthropic

client = anthropic.Anthropic()

# Define tools (Claude format)
tools = [
    {
        "name": "calculate",
        "description": "Performs mathematical calculations",
        "input_schema": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "Expression to calculate (e.g., 2 + 3 * 4)"
                }
            },
            "required": ["expression"]
        }
    }
]

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=tools,
    messages=[
        {"role": "user", "content": "What is 15 squared minus 100?"}
    ]
)

# Check tool use result
for block in response.content:
    if block.type == "tool_use":
        print(f"Tool: {block.name}")
        print(f"Input: {block.input}")
        # Output:
        # Tool: calculate
        # Input: {'expression': '15**2 - 100'}

Method Comparison

CriteriaJSON ModeStructured OutputsFunction Calling
Schema guaranteedNo (valid JSON only)Yes (100%)Yes (argument schema)
Use caseSimple JSON responsesComplex data extractionExternal function integration
Model supportMost modelsGPT-4o and select modelsMost models
Pydantic integrationManual parsingAuto parsingManual parsing
Nested structuresPossibleFully supportedPossible

Practical Tips

  • Use Structured Outputs first: Since schema compliance is 100% guaranteed, you don’t need parsing error handling code.
  • Actively use Pydantic models: You get type hints and validation simultaneously, plus IDE autocomplete support.
  • Write specific function descriptions: The description field is the key criterion the LLM uses to select the right tool. Include both “what it does” and “when to use it.”
  • Specify required fields: Without specifying required arguments, the LLM may arbitrarily omit them.
  • Use Enums to constrain choices: Instead of free text, constraining allowed values with enum produces consistent output.
  • Have a fallback strategy: For models that don’t support Structured Outputs, use a JSON mode + Pydantic validation combination as an alternative.

Was this article helpful?