Compile Ready
All AI system design lessons
Generative AI/Level 2 · Working with LLMs

Structured Outputs

Guaranteeing schema-valid responses — JSON Schema, response formats, and refusal handling for reliable pipelines.

Intermediate 35m interview 14m read High frequency Popularity 85
Prompt Engineering AI Evaluation OpenAI Anthropic

Introduction

Structured outputs turn an LLM response from best-effort text into a contract that application code can parse. Instead of asking the model to please return JSON, the application supplies a schema and the provider constrains generation so the normal response conforms to that schema.

For a strong engineer, the key distinction is reliability. Plain JSON mode usually guarantees syntactically valid JSON, but it does not guarantee the fields, enum values, required properties, or nesting your code expects. Structured outputs use a provider-supported schema subset, often JSON Schema, and can reject or separately surface safety refusals rather than mixing them into the data shape.

In production, structured outputs are the bridge between probabilistic language and deterministic software. They are used for extraction, classification, routing, tool inputs, workflow decisions, and UI-ready objects. They reduce parsing failures, but they do not remove the need for validation, observability, fallback, and careful schema design.

Where this shows up in production

Structured outputs show up anywhere an LLM result feeds code instead of being shown directly to a human.

  • Support systems classify tickets into priority, sentiment, product area, and escalation reason.
  • Sales workflows extract accounts, contacts, next steps, and confidence labels from notes.
  • RAG applications return answer objects with citations, missing-evidence flags, and follow-up questions.
  • Agent frameworks require tool arguments to match strict input schemas before executing actions.
  • Evaluation pipelines score outputs against typed rubrics instead of scraping prose.

Learning Objectives

  • Explain how schema-constrained responses differ from ordinary JSON mode.

  • Use OpenAI response_format json_schema with strict:true, Anthropic tool-based structuring, and Gemini responseSchema patterns.

  • Describe guided decoding as token masking against a schema-derived grammar.

  • Design schemas with required fields, additionalProperties:false, enums, nullable fields, and shallow structure.

  • Handle safety refusals separately from successful parsed objects.

  • Apply validation, repair, logging, and fallback as defense in depth even when strict schemas are enabled.

Theory & Concepts

Structured output is a schema contract

A structured output request gives the model an explicit target shape. With OpenAI, that commonly means response_format with type json_schema, a named schema, and strict:true. With Anthropic, the most reliable pattern is tool-based structuring: define a tool input schema, instruct the model to use that tool, then read the tool input as the structured object. With Gemini, generation config can set a JSON MIME type and responseSchema so the model emits data matching the declared schema.

The practical effect is that downstream code no longer scrapes natural language. It receives an object such as a classification, extraction, route decision, or rubric score. The schema becomes an API boundary between the LLM and the rest of the system. That boundary should be reviewed, versioned, tested, and monitored like any other interface.

Strict schemas are different from JSON mode

Plain JSON mode is useful but weaker. It tells the model to produce valid JSON, so the output is usually parseable by a JSON parser. It does not guarantee that priority is one of low, medium, or high, that required fields are present, that extra fields are absent, or that nested objects match your domain model.

Structured outputs add schema-level constraints. A strict schema can require fields, disallow unknown properties with additionalProperties:false, constrain strings with enums, and express limited unions with anyOf. If software depends on exact keys and values, structured outputs are the correct default; JSON mode is mainly for loose human-assisted workflows or quick prototypes.

Guided decoding masks invalid tokens

Under the hood, providers compile the supported schema into a grammar, finite-state machine, or similar constraint representation. At each decoding step, the sampler only allows tokens that can still lead to a valid object. Tokens that would break the schema are masked out before sampling.

That is why schema-constrained decoding can guarantee parseability for normal completions. If the next legal character must be a quote, a comma, a closing brace, or one of a small set of enum values, the model cannot freely emit arbitrary prose. The model still chooses among allowed continuations, so the content can be wrong or low quality, but the container is valid.

Provider APIs expose the idea differently

OpenAI exposes strict structured output through response_format json_schema, including a schema name and strict:true. Modern SDKs may also provide parse helpers that return a typed parsed object when the response is successful.

Anthropic often uses tools for structure. A tool definition includes an input_schema, and the assistant emits a tool_use block with JSON input. You may force a specific tool choice when the only acceptable successful output is that structured payload. Gemini supports responseSchema with response MIME type application/json, so the response is generated as JSON matching the schema shape supported by the API. The names differ, but the application pattern is the same: declare the shape, parse only the normal structured channel, and treat refusals or safety blocks separately.

Schema subset limits are product constraints

Provider-supported schemas are intentionally narrower than full JSON Schema. Common constraints include object types, properties, required arrays, additionalProperties:false, enums, arrays, primitive types, and limited anyOf unions. Some providers require every property to be listed as required, with optionality represented by an explicit nullable union such as string or null.

Advanced JSON Schema features may be rejected or ignored: arbitrary patternProperties, complex conditional validation, deeply recursive definitions, unbounded recursion, very deep nesting, huge enums, or root-level unions. Treat the provider schema subset as a product constraint. Keep schemas small, explicit, and close to what the model must decide.

Refusal is not a malformed object

A safety refusal should not be forced into your business schema. If the user asks for disallowed content, the provider can surface a refusal or safety block through a separate field, message part, stop reason, or candidate status. In that case, the application should not attempt to parse the normal schema payload.

This separation is important for correctness and security. A normal parsed object means the model accepted the task and produced schema-shaped data. A refusal means the task was not completed as structured data. Your code path should branch early: handle refusal UX, audit the event, and avoid feeding a fake parsed object into downstream automation.

Request Flow

  1. 1

    1. Choose the business object

    Start with the object the application actually needs: a ticket triage result, extracted invoice fields, a moderation decision, or a search query plan. Do not begin with prose and hope to parse it later.

  2. 2

    2. Design the schema

    Define object properties, required fields, enum values, array item shapes, and additionalProperties:false. Prefer explicit nullable fields over omitted optional fields when the provider expects all fields to be required.

  3. 3

    3. Attach the schema to the provider call

    For OpenAI, send response_format with type json_schema and strict:true. For Anthropic, define a tool with an input_schema and force or strongly prefer that tool. For Gemini, set responseSchema with application/json response MIME type.

  4. 4

    4. The provider constrains decoding

    During generation, the decoder masks tokens that would violate the schema-derived grammar. The model can still choose values among legal continuations, but it cannot emit invalid JSON structure in the normal path.

  5. 5

    5. Check for refusal or safety stop

    Before parsing, inspect provider-specific refusal and safety fields. If the model refused or the response was blocked, route to a refusal UX or fallback path instead of trying to coerce it into the schema.

  6. 6

    6. Parse and validate

    Parse the structured channel into an object and run application-side validation. Validation catches provider regressions, SDK misuse, schema version mismatches, domain constraints, and any relaxed behavior in non-strict fallback paths.

  7. 7

    7. Use the object in deterministic code

    Only after successful parsing and validation should the object drive workflow actions, database writes, tool calls, UI rendering, or analytics. Keep raw model output and parsed data separate in logs.

  8. 8

    8. Monitor and iterate

    Track refusal rate, validation failures, enum distributions, missing-evidence flags, latency, token usage, and repair retries. Schema changes should be versioned because they change both model behavior and downstream expectations.

Deep Dive

Constrained decoding improves syntax, not truth

Guided decoding can guarantee that the response is parseable and schema-shaped, but it cannot guarantee that the extracted date is correct, that the classification is fair, or that the answer is grounded in evidence. The model still predicts content based on prompt, context, and learned behavior.

This distinction matters in interviews. Structured outputs solve interface reliability. They do not solve factuality, authorization, prompt injection, or business-rule enforcement. You still need grounding, retrieval quality, server-side checks, and evaluation.

Enums are stronger than free text

If a downstream workflow has known states, use enums. A priority field with low, medium, high, and urgent is easier to validate, measure, and route than a free-text priority_reason field that the model can phrase in many ways.

Enums also reduce decoding ambiguity. The model chooses from a small set of allowed continuations, which improves repeatability and reduces accidental synonyms. Free text should be reserved for human-readable explanations, evidence snippets, or fields where open-ended language is truly required.

Nullable fields beat hidden optionality

Many strict schema implementations work best when every property is required. That can feel odd for optional fields, but it is usually better to require the field and allow null than to let the key disappear.

Explicit null has product value. It forces the prompt and schema to define what missing evidence means. For example, renewal_date can be a string or null, and a separate missing_fields array can explain what evidence was absent. Downstream code no longer guesses whether a missing key means unknown, unsupported, or model error.

Deep nesting increases model and schema risk

A deeply nested schema may be valid for software but hard for the model to fill consistently. Every nested level increases the number of braces, arrays, required fields, and decisions the decoder must satisfy. It can also make provider limits more likely.

Prefer flat or moderately nested objects for LLM outputs. If the business object is complex, split the task into stages: extract core facts first, validate them, then call a second schema for enrichment or planning. Smaller schemas are easier to test and easier to repair.

Repair loops are still useful

Strict structured outputs reduce malformed JSON, but repair remains useful for non-strict fallbacks, provider outages, domain validation failures, and business constraints that are outside the schema subset. For example, a schema can require a date string, but your validator may reject dates before account creation.

A good repair loop is bounded and observable. Send the validation error, the original task, and the invalid object to a repair prompt with the same schema. Retry once or twice, then fall back to a human review queue or safe default. Infinite repair loops turn model uncertainty into latency and cost.

Production Considerations

Version schemas and prompts together

Schema changes are API changes. Adding an enum value, renaming a field, or changing nullable behavior can break parsers, dashboards, evaluation cases, and downstream workflow rules. Record schema_version with each request and tie it to the prompt and model version.

Log parse and refusal outcomes

Track whether each request ended as parsed, refused, blocked, validation_failed, repaired, or fallback. This makes quality regressions visible. A rising validation failure rate usually indicates prompt drift, schema mismatch, a provider behavior change, or new user inputs not represented in tests.

Keep deterministic checks outside the model

Do not ask the model to enforce permissions, billing policy, or irreversible side-effect rules just because the output is structured. Use application code for authorization, quotas, idempotency, data access, and final tool execution decisions.

Budget for latency and token overhead

Schemas consume request tokens and constrained decoding can add latency depending on provider implementation, schema size, and enum complexity. The tradeoff is often worth it for automation, but high-volume paths should measure cost, latency, and failure rate against simpler classifiers or deterministic rules.

Interview Perspective

What interviewers look for

  • A crisp distinction between JSON mode and schema-guaranteed structured output.
  • Knowledge of provider-specific mechanisms: OpenAI response_format json_schema strict:true, Anthropic tool input schemas, and Gemini responseSchema.
  • Understanding that guided decoding masks invalid next tokens but does not guarantee semantic truth.
  • Production instincts around refusal handling, validation, schema versioning, observability, repair, and fallback.

Alternative designs

Plain JSON mode plus validator

The model is asked for valid JSON and application code validates the result. This is simple and portable, but failures are more common because decoding is not constrained to the exact schema. It can be acceptable for low-risk internal tools or prototypes.

Strict structured output

The provider constrains the normal response to a declared schema. This is the best default when code depends on fields, enums, and object shape. It requires learning provider schema limits and designing compact schemas.

Tool-call-only structuring

The application represents the desired object as a tool input and reads the tool_use arguments instead of assistant prose. This fits agent systems and Anthropic-style APIs well, especially when the same schema later drives a real tool execution step.

Likely follow-up questions

How does constrained decoding guarantee schema-valid JSON?

The provider converts the supported schema into a grammar or state machine. At each generation step, tokens that cannot lead to a valid object are masked out before sampling. The model still chooses content among legal continuations, but invalid braces, missing required keys, extra properties, or enum values outside the schema are not available in the normal structured path.

What is the difference between JSON mode and structured outputs?

JSON mode targets syntactic JSON. It helps prevent prose around the answer, but the object may still miss fields, add extra fields, or use unexpected values. Structured outputs target a schema, so required fields, property names, allowed enum values, object shapes, and additionalProperties:false can be enforced when supported by the provider.

How should a service handle a safety refusal?

Check the provider refusal or safety status before parsing. If the response is refused or blocked, route to a refusal UX, audit log, or fallback. Do not fabricate a schema-shaped object, and do not treat refusal text as a normal parsed payload.

Why validate if strict:true already guarantees the schema?

Validation is defense in depth. It catches SDK misuse, provider configuration mistakes, schema-version mismatches, non-strict fallback paths, and business rules outside the provider schema subset. It also gives you metrics and repair triggers when inputs drift.

Common mistakes

  • ×Confusing valid JSON with schema-valid structured output.
  • ×Designing large, deeply nested schemas that exceed provider limits or confuse the model.
  • ×Ignoring refusals and trying to parse every response as a normal object.
  • ×Skipping validation, logging, and repair because the provider call uses strict mode.

Interactive Playground

This static playground shows a ticket triage schema that favors enums and explicit nulls. The same shape can be used with OpenAI strict json_schema, Anthropic tool input schemas, or Gemini responseSchema.

System prompt

You classify customer support notes into a strict triage object.
Use only the note provided by the user.
If evidence is missing, use null for the field and add the field name to missing_fields.
Do not invent account policy or billing outcomes.

User prompt

Customer note:
I upgraded yesterday and now my invoice shows two workspace seats. I only have one employee using the product. The billing page says the change renews tomorrow, and I need to know whether I will be charged twice.

Return the triage object using the provided schema.

model

gpt-4o-mini

A smaller model is often enough for short extraction when the schema is tight.

response_format.type

json_schema

OpenAI structured output mode for a JSON Schema contract.

response_format.json_schema.strict

true

Rejects unsupported loose output and constrains normal decoding to the schema.

temperature

0.1

Low randomness improves repeatability for routing decisions.

Sample output

A successful normal response would parse as an object like this:

{ "category": "billing", "priority": "medium", "customer_sentiment": "concerned", "requires_human_review": true, "renewal_date": "tomorrow", "missing_fields": ["billing_policy"], "summary": "Customer sees two seats after upgrade and worries about being charged twice." }

If the request were unsafe or disallowed, the provider should surface a refusal or safety block separately. The application should not try to force that refusal into this object.

Visual Learning

Output reliability modes

ModeGuaranteeBest useMain risk
Free-form textNo machine-readable guaranteeHuman-facing explanationsBrittle parsing and hidden assumptions
JSON modeValid JSON syntax in normal casesLoose prototypes and human-assisted workflowsFields and values may not match expectations
Strict structured outputSupported schema shape for normal responsesAutomation, routing, extraction, and typed UI objectsSchema subset limits and semantic errors still apply
Tool input schemaTool arguments match declared input shape when tool is usedAgents and action workflowsTool choice and refusal handling must be explicit

Provider patterns

ProviderMechanismNormal structured channelRefusal or safety path
OpenAIresponse_format json_schema with strict:trueMessage content or SDK parsed object matching the schemaRefusal field or safety-related response path before parsing
AnthropicTool definition with input_schema and tool choicetool_use block inputText refusal, stop reason, or no tool_use block
GeminiresponseSchema with application/json response MIME typeJSON response matching supported schemaBlocked candidate or safety metadata
Portable fallbackPrompt asks for JSON plus local validatorParsed JSON if validation succeedsValidation failure, repair retry, or human review

Schema design choices

ChoicePreferAvoidWhy it matters
LabelsEnums for known categoriesOpen-ended labels for routingEnums reduce synonyms and simplify metrics
Optional dataRequired field with explicit null when unknownMissing keys with unclear meaningDownstream code can distinguish unknown from parser failure
ShapeFlat or moderately nested objectsDeep recursive structuresSmaller schemas decode faster and fail less often
Extra fieldsadditionalProperties:falseAllowing arbitrary model-invented keysUnexpected keys create integration and security risk
ExplanationsShort rationale fields tied to evidenceLong prose mixed with control fieldsKeeps automation data separate from human-readable context

Decision guide

Choosing an output strategy

Use strict structured outputs when application code depends on exact fields, enum values, arrays, or object shapes. This is the default for extraction, classification, routing, and any workflow that writes to a database or calls tools.

Use tool-based structuring when the structured object is naturally a tool input or when the provider exposes its strongest schema guarantees through tools. This is common in Anthropic workflows and agent systems.

Use Gemini responseSchema when building on Gemini and the response should be JSON matching a declared schema. Pair it with response MIME type application/json and local validation.

Use JSON mode only when valid JSON is enough and schema drift is tolerable, such as early prototypes, internal analysis, or human-reviewed output. Add a local validator and repair loop if the result feeds code.

Keep schemas small, explicit, and boring. Prefer enums over free text, explicit null over missing keys, additionalProperties:false over open objects, and shallow structures over deep nesting. If the object becomes large, split the workflow into multiple schema-constrained calls.

Always branch on refusal before parse, then validate, log, and apply deterministic business rules outside the model.

Hands-on Examples

Define a strict JSON schema and parse the result

This Python example defines a schema for ticket triage, sends it as an OpenAI strict json_schema response_format, checks for refusal first, then parses the normal response. It uses string concatenation and ordinary JSON parsing so the control flow is clear.

openai_structured_output.py

import json
from openai import OpenAI

client = OpenAI()

triage_schema = {
    "type": "object",
    "additionalProperties": False,
    "properties": {
        "category": {
            "type": "string",
            "enum": ["billing", "technical", "account", "sales"]
        },
        "priority": {
            "type": "string",
            "enum": ["low", "medium", "high", "urgent"]
        },
        "requires_human_review": {
            "type": "boolean"
        },
        "renewal_date": {
            "anyOf": [
                {"type": "string"},
                {"type": "null"}
            ]
        },
        "missing_fields": {
            "type": "array",
            "items": {"type": "string"}
        },
        "summary": {
            "type": "string"
        }
    },
    "required": [
        "category",
        "priority",
        "requires_human_review",
        "renewal_date",
        "missing_fields",
        "summary"
    ]
}

note = (
    "I upgraded yesterday and now my invoice shows two workspace seats. "
    + "I only have one employee using the product. "
    + "The billing page says the change renews tomorrow."
)

completion = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {
            "role": "system",
            "content": "Classify the support note. Use null when evidence is missing."
        },
        {
            "role": "user",
            "content": "Support note: " + note
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "support_ticket_triage",
            "strict": True,
            "schema": triage_schema
        }
    }
)

message = completion.choices[0].message

if getattr(message, "refusal", None):
    print("Refusal: " + message.refusal)
else:
    parsed = json.loads(message.content)
    print("Priority: " + parsed["priority"])
    print("Category: " + parsed["category"])
    print("Missing fields: " + ", ".join(parsed["missing_fields"]))

Validate and repair after parsing

Strict schemas protect the interface, while application validation protects product rules. This example rejects an object that is schema-shaped but violates a domain rule, then prepares a bounded repair prompt.

validate_and_repair.py

allowed_categories = ["billing", "technical", "account", "sales"]
allowed_priorities = ["low", "medium", "high", "urgent"]

def validate_triage(obj):
    errors = []
    if obj.get("category") not in allowed_categories:
        errors.append("category must be one of " + ", ".join(allowed_categories))
    if obj.get("priority") not in allowed_priorities:
        errors.append("priority must be one of " + ", ".join(allowed_priorities))
    if obj.get("priority") == "urgent" and obj.get("requires_human_review") is False:
        errors.append("urgent tickets must require human review")
    if len(obj.get("summary", "")) > 180:
        errors.append("summary must be 180 characters or fewer")
    return errors

def build_repair_prompt(original_note, invalid_obj, errors):
    parts = []
    parts.append("Repair the triage object so it satisfies the schema and product rules.")
    parts.append("Use only the original support note as evidence.")
    parts.append("Original support note:")
    parts.append(original_note)
    parts.append("Invalid object:")
    parts.append(str(invalid_obj))
    parts.append("Validation errors:")
    parts.append("; ".join(errors))
    parts.append("Return only the corrected structured object.")
    return "\n".join(parts)

candidate = {
    "category": "billing",
    "priority": "urgent",
    "requires_human_review": False,
    "renewal_date": "tomorrow",
    "missing_fields": ["billing_policy"],
    "summary": "Customer worries about being charged for two seats after an upgrade."
}

note = "Customer sees two seats after an upgrade and asks whether billing will double."
errors = validate_triage(candidate)

if errors:
    repair_prompt = build_repair_prompt(note, candidate, errors)
    print(repair_prompt)
else:
    print("Object is ready for deterministic workflow code.")

Model the same object as a tool input

Tool-based structuring represents the desired payload as tool arguments. In an Anthropic-style flow, the application reads the tool_use input rather than parsing assistant prose.

tool_schema_shape.py

triage_tool = {
    "name": "record_ticket_triage",
    "description": "Record the structured triage result for one support ticket.",
    "input_schema": {
        "type": "object",
        "additionalProperties": False,
        "properties": {
            "category": {
                "type": "string",
                "enum": ["billing", "technical", "account", "sales"]
            },
            "priority": {
                "type": "string",
                "enum": ["low", "medium", "high", "urgent"]
            },
            "requires_human_review": {
                "type": "boolean"
            },
            "summary": {
                "type": "string"
            }
        },
        "required": ["category", "priority", "requires_human_review", "summary"]
    }
}

def find_tool_input(content_blocks, tool_name):
    for block in content_blocks:
        if block.get("type") == "tool_use" and block.get("name") == tool_name:
            return block.get("input")
    return None

example_blocks = [
    {
        "type": "tool_use",
        "name": "record_ticket_triage",
        "input": {
            "category": "billing",
            "priority": "medium",
            "requires_human_review": True,
            "summary": "Customer asks whether two seats after upgrade will double billing."
        }
    }
]

tool_input = find_tool_input(example_blocks, triage_tool["name"])

if tool_input is None:
    print("No structured tool input was produced; handle refusal or retry.")
else:
    print("Structured tool input category: " + tool_input["category"])

Quiz

0/6 answered

  1. 1.What does strict structured output guarantee that plain JSON mode does not?

  2. 2.How does guided decoding usually enforce a schema?

  3. 3.Which schema design is usually best for a known routing category?

  4. 4.What should code do before parsing a structured response?

  5. 5.Why is additionalProperties:false useful?

  6. 6.Why keep validation even when strict schemas are enabled?

Flashcards

Cheat Sheet

Structured outputs cheat sheet

Core distinction

  • Free text: best for human-facing prose, worst for automation.
  • JSON mode: produces valid JSON, but fields and values can drift.
  • Structured output: constrains the normal response to a supported schema.
  • Tool input schema: treats the structured payload as tool arguments.

Provider names

  • OpenAI: response_format type json_schema, json_schema name, strict:true, schema.
  • Anthropic: tool definition with input_schema, then read tool_use input.
  • Gemini: responseSchema with response MIME type application/json.

Guided decoding

  1. Provider compiles the supported schema into a grammar or state machine.
  2. Decoder computes legal next tokens for the current state.
  3. Invalid tokens are masked before sampling.
  4. The normal response remains parseable, but the content still needs quality checks.

Schema design

  • Use object schemas with explicit properties.
  • Mark fields required when the provider expects it.
  • Use additionalProperties:false to prevent surprise keys.
  • Use enums for labels, routes, statuses, and priorities.
  • Use anyOf with null for explicit nullable fields.
  • Prefer shallow schemas over deep recursive structures.
  • Keep explanations short and separate from control fields.

Refusal handling

  • Check refusal, safety metadata, stop reason, or missing structured channel before parsing.
  • Do not coerce a refusal into a fake business object.
  • Log refused, blocked, parsed, validation_failed, repaired, and fallback outcomes separately.

Defense in depth

  • Validate parsed objects locally.
  • Enforce authorization and business rules in code.
  • Use bounded repair retries for validation failures.
  • Version schema, prompt, and model together.
  • Monitor enum distributions, null rates, repair rate, refusal rate, latency, and cost.

Interview answer shape

  1. Define structured outputs versus JSON mode.
  2. Name provider mechanisms.
  3. Explain guided decoding token masking.
  4. Discuss schema subset limits.
  5. Branch on refusal before parse.
  6. Validate, repair, log, and fallback in production.

References