Level 4Lesson 30⏱️ 90 min

Structured Outputs & Tool Use

Make Claude return machine-readable data and call your functions — the foundation of every real AI feature.

Why Unstructured Text Isn't Enough

Claude is a great writer. But your app is not a reader — it needs JSON. Structured outputs and tool use are two ways to get reliable, parseable data back from Claude instead of a wall of prose.

The two techniques:
  • Structured outputs — tell Claude to respond in a specific JSON schema
  • Tool use — give Claude functions it can call; you execute them, return results

Technique 1: Structured Outputs (JSON Mode)

The simplest approach: add a schema to your system prompt and ask Claude to respond with valid JSON. Works in every SDK version.

import anthropic

client = anthropic.Anthropic()

def extract_contact(text: str) -> dict:
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=256,
        system="""Extract contact info and return ONLY valid JSON:
{
  "name": "string or null",
  "email": "string or null",
  "phone": "string or null",
  "company": "string or null"
}
No markdown, no explanation — raw JSON only.""",
        messages=[{"role": "user", "content": text}]
    )
    import json
    return json.loads(response.content[0].text)

result = extract_contact(
    "Hi, I'm Sarah Chen, CTO at Acme Corp. "
    "Reach me at sarah@acme.io or 555-0192."
)
print(result)
# {'name': 'Sarah Chen', 'email': 'sarah@acme.io',
#  'phone': '555-0192', 'company': 'Acme Corp'}
Pro tips for reliable JSON:
  • Say "ONLY valid JSON" and "no markdown" in the system prompt
  • Paste the exact schema in the prompt — Claude will follow it closely
  • Wrap json.loads() in a try/except and retry once on failure
  • For complex schemas, use Pydantic + instructor library (see below)

Instructor: Pydantic-Validated Outputs

The instructor library patches the Anthropic client to validate Claude's JSON against a Pydantic model — automatic retries included.

# pip install instructor pydantic
import instructor
import anthropic
from pydantic import BaseModel, EmailStr
from typing import Optional

class Contact(BaseModel):
    name: Optional[str]
    email: Optional[EmailStr]
    phone: Optional[str]
    company: Optional[str]

client = instructor.from_anthropic(anthropic.Anthropic())

contact = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=256,
    response_model=Contact,
    messages=[{
        "role": "user",
        "content": "Sarah Chen, CTO at Acme Corp. sarah@acme.io, 555-0192"
    }]
)
print(contact.name)   # Sarah Chen
print(contact.email)  # sarah@acme.io

Technique 2: Tool Use (Function Calling)

Tool use lets Claude decide when to call your functions. You define the tools, Claude picks which to call and with what arguments, you execute them, and return the results. Claude then writes the final response.

1
Define your tools — name, description, and JSON Schema for parameters
2
Send to Claude — include tools array in the API call
3
Check stop reason — if tool_use, Claude wants to call a function
4
Execute and return — run the function, send result back as tool_result

Tool Use: Full Example

import anthropic, json

client = anthropic.Anthropic()

# Step 1: define tools
tools = [{
    "name": "get_weather",
    "description": "Get current weather for a city",
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "City name"},
            "units": {"type": "string", "enum": ["celsius", "fahrenheit"]}
        },
        "required": ["city"]
    }
}]

def get_weather(city: str, units: str = "celsius") -> dict:
    # Your real implementation here
    return {"city": city, "temp": 22, "condition": "Sunny", "units": units}

messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]

# Step 2: first call
response = client.messages.create(
    model="claude-opus-4-5", max_tokens=512,
    tools=tools, messages=messages
)

# Step 3: handle tool calls
if response.stop_reason == "tool_use":
    tool_use = next(b for b in response.content if b.type == "tool_use")
    result = get_weather(**tool_use.input)   # Step 4: execute

    messages += [
        {"role": "assistant", "content": response.content},
        {"role": "user", "content": [{
            "type": "tool_result",
            "tool_use_id": tool_use.id,
            "content": json.dumps(result)
        }]}
    ]
    final = client.messages.create(
        model="claude-opus-4-5", max_tokens=512,
        tools=tools, messages=messages
    )
    print(final.content[0].text)
    # "Tokyo is currently 22 degrees Celsius and sunny."

Tool Use in TypeScript (Next.js API Route)

// app/api/chat/route.ts
import Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic()

const tools: Anthropic.Tool[] = [{
  name: 'search_products',
  description: 'Search the product catalogue',
  input_schema: {
    type: 'object' as const,
    properties: {
      query: { type: 'string' },
      max_results: { type: 'number' }
    },
    required: ['query']
  }
}]

async function searchProducts(query: string, maxResults = 5) {
  // call your DB / search API here
  return [{ id: 1, name: 'Widget A', price: 29.99 }]
}

export async function POST(req: Request) {
  const { messages } = await req.json()
  const response = await client.messages.create({
    model: 'claude-opus-4-5', max_tokens: 1024,
    tools, messages
  })

  if (response.stop_reason === 'tool_use') {
    const toolBlock = response.content.find(b => b.type === 'tool_use')!
    const results = await searchProducts(
      (toolBlock as any).input.query,
      (toolBlock as any).input.max_results
    )
    const followUp = await client.messages.create({
      model: 'claude-opus-4-5', max_tokens: 1024,
      tools,
      messages: [
        ...messages,
        { role: 'assistant', content: response.content },
        { role: 'user', content: [{
          type: 'tool_result',
          tool_use_id: (toolBlock as any).id,
          content: JSON.stringify(results)
        }]}
      ]
    })
    return Response.json({ reply: (followUp.content[0] as any).text })
  }
  return Response.json({ reply: (response.content[0] as any).text })
}

Hands-on: Build a Mini Data Extractor

Challenge: Build an invoice parser that takes raw invoice text and returns structured JSON with: vendor, amount, due_date, line_items[].

  1. Write the system prompt with the exact JSON schema
  2. Test with 3 different invoice formats (email, PDF text, handwritten scan OCR)
  3. Add a Pydantic model with instructor to auto-validate
  4. Handle the case where a field is missing — use Optional

Stretch: Add a tool lookup_vendor(name) that checks a local dict of known vendors and returns their standard payment terms.

Lesson 30 Quick Reference
Structured output

System prompt + JSON schema → json.loads() on response

instructor library

pip install instructor — Pydantic validation + auto-retry

tool use

Claude chooses which function to call + args; you execute it

stop_reason == tool_use

Signal that Claude wants to call a function

tool_result

Message role=user with type=tool_result to return function output

multi-turn tools

Append assistant + tool_result messages, call API again for final answer