---
title: "Structured Output"
slug: "structured-output"
category: "ai-agents"
tags: ["ai-agents", "structured-output", "json-schema", "tool-use"]
status: "stable"
last_updated: 2026-08-14
summary: "Use JSON Schema via tool use or strict mode; validate at the boundary; never parse JSON from prose with regex."
related: ["[[prompt-engineering/prompt-design]]", "[[ai-agents/system-prompts]]", "[[prompt-engineering/chain-of-thought]]", "[[ai-agents/evaluation]]"]
---

## Overview

Anything a program will consume must be JSON validated against a schema. Prose is for humans; downstream code needs typed, predictable shape. Modern APIs from Anthropic, OpenAI, and Google all support schema-constrained output. Use it. Never `regex` a JSON object out of free-form prose; you will spend a year debugging the edge cases.

## Define the schema first

Write the schema before the prompt. The schema is the contract; the prompt is the implementation detail that satisfies it. Contract-first design surfaces ambiguity early.

```json
{
  "type": "object",
  "required": ["verdict", "reasons"],
  "additionalProperties": false,
  "properties": {
    "verdict": { "enum": ["approve", "request_changes", "block"] },
    "reasons": {
      "type": "array",
      "minItems": 1,
      "maxItems": 5,
      "items": { "type": "string", "maxLength": 200 }
    }
  }
}
```

Bounded enums, bounded arrays, bounded strings. Every field that does not have a max is a future incident.

## Use tool use or strict mode, not prose-with-JSON

Every major provider has a schema-constrained mode. Use it.

- Anthropic: [[glossary/tool-use|tool use]] with an input schema; the model can only emit matching JSON.
- OpenAI: `response_format: { type: "json_schema", strict: true }` or [[glossary/function-calling|function calling]] with `strict: true`.
- Google Gemini: `responseMimeType: "application/json"` plus `responseSchema`.

"Return JSON in this shape" in a system prompt is a hope, not a contract. Strict modes enforce it at the decoder.

## Validate at the boundary

Treat the model like an untrusted external service. Validate every response against the schema before any downstream code touches it.

- Python: `pydantic.BaseModel.model_validate(payload)`. The `instructor` library wraps this around the API call.
- TypeScript: `zod.parse(payload)` or `ajv` with the same JSON Schema you sent the model. See [[coding/typescript-runtime-validation]].
- On validation failure: retry with the validation error in the next user message, or fall back to a smaller model.

Validation is cheap; cleanup of a bad row written into Postgres is not. See [[backend/postgres]].

## Never parse JSON from prose with regex

A regex that pulls `{...}` out of a model response will fail on nested braces, code fences, escaped quotes, multi-line strings, and the day the model emits two JSON objects. The structured-output APIs exist for a reason.

- If you must extract JSON from prose, parse a JSON balanced substring with a stack-based parser, not regex.
- If you must do that on a hot path, switch to tool-use mode. The work is already done for you.

The cost of switching to tool-use is one afternoon. The cost of regex-parsed JSON in production is six months of `JSONDecodeError` alerts.

## Strict mode catches type drift

Strict mode rejects unknown properties, type mismatches, and unbounded arrays at the decoder. Without it, the model invents fields when it gets uncertain.

- Anthropic tool use: define the input schema with `additionalProperties: false`. Unknown keys are not generated.
- OpenAI strict mode: every field must be declared; `additionalProperties` defaults to `false`. The model cannot add a `"note"` field on a whim.

Loose schemas drift. A `"metadata": {}` open object becomes a junk drawer. Constrain or do not include.

## Combine reasoning with a structured answer

When the task benefits from chain-of-thought, put the reasoning in one field and the answer in another. The schema separates concerns; the caller reads the answer; the log keeps the reasoning. See [[prompt-engineering/chain-of-thought]].

```json
{
  "type": "object",
  "required": ["reasoning", "answer"],
  "properties": {
    "reasoning": { "type": "string", "maxLength": 2000 },
    "answer": { "type": "string", "maxLength": 200 }
  }
}
```

Without the structure, the reasoning leaks into the user-facing answer or vanishes from the logs entirely.

## Pydantic plus instructor is the Python default

For Python services, the pairing of `pydantic` for the schema and `instructor` for the API client gives you typed models, retries on validation failure, and structured output across providers with one interface.

```python
import instructor
from pydantic import BaseModel, Field
from anthropic import Anthropic

class Review(BaseModel):
    verdict: str = Field(pattern="^(approve|request_changes|block)$")
    reasons: list[str] = Field(min_length=1, max_length=5)

client = instructor.from_anthropic(Anthropic())
result = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    response_model=Review,
    messages=[{"role": "user", "content": "Review PR #401"}],
)
```

The Pydantic model is the schema and the type at the same time. See [[backend/fastapi]] for the same pattern at the API boundary.

## Version the schema like an API

A schema is a public interface. Treat schema changes like API breaks.

- Add fields with safe defaults; never repurpose a field.
- Rename only via additive deprecation: new field, parallel run, retire old field.
- Tag the schema version in logs so a downstream parser can choose the right decoder.

A schema change without a version bump is the same incident as an unannounced REST breaking change.

## Related

- [[prompt-engineering/prompt-design]]
- [[ai-agents/system-prompts]]
- [[prompt-engineering/chain-of-thought]]
- [[ai-agents/evaluation]]
