Claude for Developers

Lesson 6 of 24

Structured output you can build on

The moment Claude's output feeds a program instead of a person, "usually valid JSON" becomes a bug class: markdown fences around the payload, a trailing comment, a missing field — each one a 3 a.m. parser exception. Structured outputs close the class: you hand the API a JSON Schema and the response is guaranteed to validate against it. Not prompted to, guaranteed to.

The old trick is gone; the real feature replaced it

For years the workaround was prefilling — starting the assistant turn with {"name": " to force JSON. Prefills return an error on current models, and the replacement is strictly better because it is enforced by the API rather than suggested to the model. The canonical form puts the schema in output_config.format:

python
response = client.messages.create(
    model="claude-opus-5",
    max_tokens=16000,
    messages=[{"role": "user", "content": "Extract: Jane Doe (jane@co.com) wants the Enterprise plan, demo requested."}],
    output_config={
        "format": {
            "type": "json_schema",
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                    "plan": {"type": "string"},
                    "demo_requested": {"type": "boolean"},
                },
                "required": ["name", "email", "plan", "demo_requested"],
                "additionalProperties": False,
            },
        }
    },
)

The response's text block contains JSON that parses and validates, every time.

Let your type system write the schema

Hand-writing JSON Schema is busywork with failure modes. The SDKs generate it from the types you already have — Pydantic in Python, Zod in TypeScript — and hand you a validated object instead of a string:

python
from pydantic import BaseModel

class ContactInfo(BaseModel):
    name: str
    email: str
    plan: str
    demo_requested: bool

response = client.messages.parse(
    model="claude-opus-5",
    max_tokens=16000,
    messages=[{"role": "user", "content": "Extract: Jane Doe (jane@co.com) wants Enterprise, demo requested."}],
    output_format=ContactInfo,
)

contact = response.parsed_output   # a validated ContactInfo instance
print(contact.email)

This is the recommended shape for every extraction pipeline: one model class serves as prompt contract, wire schema, and parsed return type — three things that can no longer drift apart.

Strict tool inputs: the same guarantee, other direction

Structured outputs constrain what Claude says; the sibling feature constrains what Claude passes to your functions. Add strict: true to a tool definition (with additionalProperties: false and a required list) and the tool's input is guaranteed to validate against your schema — no more defensive parsing of half-right arguments inside every tool handler. When you reach the tool-use lesson, treat strict: true as the default for any tool whose arguments matter.

Know the schema dialect's edges

The guarantee covers a defined subset of JSON Schema, and the edges are worth memorising before you design around them:

  • Supported: the basic types, enum, const, anyOf/allOf, $ref, common string formats (date-time, email, uuid, uri), and additionalProperties: false — which is required on every object.
  • Not supported: recursive schemas, numeric bounds (minimum/maximum), and string length constraints. The Python and TypeScript SDKs quietly strip unsupported constraints from what they send and validate them client-side instead — convenient, but know that the API-level guarantee doesn't include your minLength.

Three operational notes. A new schema pays a one-time compilation cost on first use, then caches for 24 hours — so don't generate schemas dynamically per request. If the response reports stop_reason: "max_tokens", the JSON may be cut off mid-structure — size the cap generously. And a safety refusal can override the schema, which is one more reason the habit from the streaming lesson — check stop_reason before parsing — is non-negotiable.

Schema design is prompt design

One more lever most teams miss: the schema itself steers the model. Field names and descriptions are read as instructions — "summary": {"type": "string", "description": "One sentence, plain language, no jargon"} shapes the value, not just its type. Enums turn open-ended judgment into forced choice, which is exactly what you want for classification. And field order nudges reasoning: putting an evidence field before a verdict field makes the model justify before it concludes — a miniature chain of thought, enforced by structure instead of prose.

What to take into the next lesson

Schema in, guaranteed-valid data out; types generate the schema; strict: true extends the same promise to tool arguments; the schema itself is a steering surface. Extraction, classification, and routing — the bread-and-butter production tasks — all collapse into this one pattern. Next, the feature that turns Claude from a text generator into a system that does things: tools.

← Previous