Dev Notes

Getting Structured Output from LLMs

The hardest part of building LLM applications is getting reliable, parseable output. Here is what works.

The Problem

You ask an LLM for JSON and get:

Sure\! Here is the JSON you requested:

```json
{"name": "John", "age": 30}

Hope that helps!


Good luck parsing that.

## Solution 1: Tool Use / Function Calling

The most reliable approach. Define a schema, let the model fill it:

```python
import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-5-20250929",
    max_tokens=1024,
    tools=[{
        "name": "extract_person",
        "description": "Extract person information from text",
        "input_schema": {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"},
                "occupation": {"type": "string"}
            },
            "required": ["name"]
        }
    }],
    tool_choice={"type": "tool", "name": "extract_person"},
    messages=[{
        "role": "user",
        "content": "John is a 30-year-old software engineer from Boston."
    }]
)

# Guaranteed valid JSON matching the schema
data = response.content[0].input
# {"name": "John", "age": 30, "occupation": "software engineer"}

This works because the model is constrained to produce valid tool input.

Solution 2: Pydantic + Instructor

The instructor library wraps tool use with Pydantic validation:

import instructor
from pydantic import BaseModel

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

class Person(BaseModel):
    name: str
    age: int | None = None
    occupation: str | None = None

person = client.messages.create(
    model="claude-sonnet-4-5-20250929",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": "John is a 30-year-old software engineer."
    }],
    response_model=Person
)

print(person.name)  # "John"
print(person.age)   # 30

Clean, type-safe, and handles retries on validation failure.

Solution 3: Prefilled Response

For Claude specifically, you can prefill the assistant response:

response = client.messages.create(
    model="claude-sonnet-4-5-20250929",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "Extract: John, 30, engineer. Return JSON."},
        {"role": "assistant", "content": "{"}
    ]
)
# Model continues from "{" and produces valid JSON
result = "{" + response.content[0].text

Hacky but effective for simple cases.

Which to Use

ApproachReliabilityComplexityBest For
Tool useHighestMediumProduction apps
Instructor/PydanticHighestLowPython apps
Prefilled responseMediumLowQuick scripts
Prompt engineeringLowLowPrototyping

Start with tool use or Instructor. Fall back to prompt engineering only for exploratory work.