TAI BUI
Back to curriculum
BuildPython

Structured Outputs: JSON, Schema Validation, Constrained Decoding

Your LLM returns a string and your application needs JSON. That gap breaks more production pipelines than hallucination does. Structured output is the bridge between natural language and typed data. Get it right and the model behaves like a typed API. Get it wrong and you end up parsing free text with regex at 3am.

TIME: ~90 minutesPREREQUISITES: Phase 10, Lessons 01-05 (LLMs from Scratch)

Learning Objectives

  • Implement JSON-mode and schema-constrained outputs using OpenAI and Anthropic API parameters
  • Build a Pydantic validation layer that rejects malformed LLM outputs and retries with error feedback
  • Explain how constrained decoding forces valid JSON at the token level without post-processing
  • Design robust extraction prompts that reliably convert unstructured text into typed data structures

The Problem

You ask an LLM: "Extract the product name, price, and availability from this text." It responds:

The product is the Sony WH-1000XM5 headphones, which cost $348.00 and are currently in stock.

That answer is correct, and your application can't use it. Your inventory system needs {"product": "Sony WH-1000XM5", "price": 348.00, "in_stock": true}. You need a JSON object with specific keys, types, and value constraints, not a sentence.

The naive solution: add "Respond in JSON" to your prompt. This works 90% of the time. The other 10% of the time, the model wraps the JSON in markdown code fences, or adds a preamble like "Here's the JSON:", or produces syntactically invalid JSON because it closed a bracket early. The parser crashes and the pipeline breaks, so you add try/except and a retry loop. The retry sometimes returns different data, and now you have a consistency problem on top of the parsing problem.

The real problem is decoding, not prompting. The model generates tokens left to right. At each position, it picks the most likely next token from a vocabulary of 100K+ options. At any given position, most of those options would produce invalid JSON. If the model just emitted {"price":, the next token must be a digit, a quote (for string), null, true, false, or a negative sign. Anything else is invalid. Without constraints, the model can pick a perfectly reasonable English word that breaks the syntax.

Warm-up0/3
1. An LLM is generating text. What decides the token it emits at the next position?Warm-up
Three words, Once upon a, feed into a box labelled LLM, followed by a question mark for the next token. Below, a row of sample candidate words: time, dog, 7, a curly brace, banana, the.
Schematic. The candidate words are illustrative.
2. A signup form sends the age field to a server as the text "ten" when the server expects a number. What does a well-built server normally do?Warm-up
A signup form with Name Ana and Age ten, a Submit button, an arrow to a server that expects a number for age, and an arrow to a question mark labelled response.
Schematic of the situation in the question.
3. Which of these is a valid JSON document?Warm-up
A box for a text to parse feeds a strict JSON parser, which has two possible outputs: a parsed value or a parse error.
Schematic. The four candidate texts are in the answer options.
Check: The Problem0/2
1. Why is getting structured JSON output from an LLM hard?Explain
A prompt asking to extract the product name, price and availability, and below it a prose reply about the Sony WH-1000XM5 headphones costing $348.00 and in stock.
Texts copied from The Problem section.
2. Why does wrapping a prompt-only call in try/except plus a retry loop create a new problem?Explain
A Python for loop that calls the model, tries json.loads and continues on a decode error. Below, attempt 1 gives a parse error and attempt 2 parses and returns. A question mark asks what new problem it adds.
Loop typed for illustration; call_model is a placeholder.

The Concept

The Structured Output Spectrum

There are four levels of structured output control, and each one is stricter than the last.

Mermaid diagram

Prompt-based ("Respond in valid JSON"): no enforcement. The model usually complies but sometimes does not. Reliability: ~90%. Failure modes: markdown fences, preamble text, truncated output, wrong structure.

JSON mode: the API guarantees the output is valid JSON. OpenAI's response_format: { type: "json_object" } enables this. The output will parse, but it may not match your schema: extra keys, wrong types, missing fields.

Schema mode: the API takes a JSON Schema and guarantees the output matches it. In 2026 every major provider supports this natively: OpenAI's response_format: { type: "json_schema", json_schema: {...} } (also as tool_choice="required"), Anthropic's tool use with input_schema, and Gemini's response_schema + response_mime_type: "application/json". The output has the exact keys, types, and constraints you specified.

Constrained decoding: at each token position during generation, the decoder masks out all tokens that would produce invalid output. If the schema requires a number and the model is about to emit a letter, that token gets probability zero, so the model can only produce tokens that lead to valid output. OpenAI's structured output mode and libraries like Outlines and Guidance work this way under the hood.

JSON Schema: The Contract Language

JSON Schema is how you tell the model (or validation layer) what shape the output must have. Every major structured output system uses it.

json
{
  "type": "object",
  "properties": {
    "product": { "type": "string" },
    "price": { "type": "number", "minimum": 0 },
    "in_stock": { "type": "boolean" },
    "categories": {
      "type": "array",
      "items": { "type": "string" }
    }
  },
  "required": ["product", "price", "in_stock"]
}

This schema says the output must be an object with a string product, a non-negative number price, a boolean in_stock, and an optional array of strings categories. Anything that doesn't match gets rejected.

Schemas also cover nested objects, arrays with typed items, enums (constrain a string to specific values), pattern matching (regex on strings), and combinators (oneOf, anyOf, allOf for polymorphic outputs).

The Pydantic Pattern

In Python, you rarely write JSON Schema by hand. You define a Pydantic model and it generates the schema.

python
from pydantic import BaseModel
 
class Product(BaseModel):
    product: str
    price: float
    in_stock: bool
    categories: list[str] = []

This produces the same JSON Schema as above. The Instructor library (and OpenAI's SDK) accept Pydantic models directly: pass the model class, get back a validated instance. If the LLM output does not match, Instructor retries automatically.

Function Calling / Tool Use

Tool use is a second interface for the same problem. Instead of asking the model to produce JSON directly, you define "tools" (functions) with typed parameters. The model outputs a function call with structured arguments. OpenAI calls this "function calling." Anthropic calls it "tool use." Either way you get structured data.

Mermaid diagram

Prefer tool use when the model needs to choose which function to call, not just fill in parameters. If you have 10 different extraction schemas and the model must pick the right one based on the input, tool use gives you both the schema selection and the structured output.

Common Failure Modes

Schema enforcement doesn't remove every failure.

Hallucinated values: the output matches the schema but contains invented data. The model produces {"price": 299.99} when the text says $348. Schema validation can't catch this, because the type is correct and only the value is wrong.

Enum confusion: you constrain a field to ["in_stock", "out_of_stock", "preorder"]. The model outputs "available" which is semantically fine but outside the allowed set. Good constrained decoding prevents this. Prompt-based approaches don't.

Nested object depth: deeply nested schemas (4+ levels) tend to produce more errors, since each level is another place for the model to lose track of structure.

Array length: the model may produce too many or too few items in an array. Schemas support minItems and maxItems, but some providers don't enforce them at the decoding level.

Optional field omission: the model omits fields that are technically optional but semantically important for your use case. Mark them required in the schema even if the data is sometimes missing, which forces the model to produce null explicitly.

Check: The Concept0/18
1. Which of these can a JSON Schema express?Define
A JSON Schema with product, price, in_stock and categories properties, and below it an object with product Sony WH-1000XM5, price 348.0 and in_stock true. A question mark asks what else a schema can describe.
Schema and object copied from the lesson.
2. What do you gain by defining a Pydantic model for LLM output?Define
A Pydantic class Product with fields product, price, in_stock and categories, and an arrow to a box asking what you gain from it.
Class copied from the lesson.
3. What is JSON mode?Define
The request setting response_format with type json_object, leading to a model box and then to an output guarantee marked with a question mark.
Setting copied from the lesson.
4. What is structured output, as the lesson uses the term?Define
A prose sentence about the Sony WH-1000XM5 headphones above, and below it a JSON object with product, price and in_stock, as the inventory system needs.
Both texts copied from The Problem section.
5. What is constrained decoding?Define
A model box emits tokens one after another: a curly brace, price in quotes, a colon, then more. A question mark asks what the decoder does at each position.
Schematic.
6. What does token masking do?Define
Five horizontal bars of next-token chances after the price key: 3 at 0.30, space at 0.15, minus at 0.05, approximately at 0.35 and the at 0.15. A question mark asks what the masking step does.
Illustrative values, not from a real model.
7. What is function calling (tool use) in this lesson?Define
A condensed request defining a tool extract_product with product, price and in_stock, and a user message Extract: Sony WH-1000XM5, $348, in stock. An arrow goes to a model box and then to a box asking what comes back.
Condensed from the lesson's example; typed.
8. What does schema compliance mean?Define
The product schema from the lesson, and below it an object with product Sony WH-1000XM5, price 348.0 and in_stock true. A question mark asks what schema compliance covers.
Schema and object copied from the lesson.
9. In Anthropic's API, how do you get output that matches a schema?Define
A product schema, and below it a Python messages.create call with model, max_tokens and messages, and one argument replaced by question marks.
Condensed from the lesson's Anthropic example; the blank is ours.
10. How does schema mode differ from JSON mode?Explain
Two request settings side by side. JSON mode sets response_format type json_object. Schema mode sets type json_schema with a json_schema object. A question mark under each asks what it guarantees.
Settings as written in The Structured Output Spectrum.
11. Why is constrained decoding stronger than validating the output after generation?Explain
Two rows of the same five tokens. The top row ends in a validator box after the last token. The bottom row has a question mark asking what differs for constrained decoding.
Schematic.
12. Why does the lesson advise marking a sometimes-missing field as required in the schema?Explain
Source text Sony WH-1000XM5, $348, in stock with no category, the categories schema field, and a choice between leaving categories out of the required list or listing it. A question mark asks about the output.
Text and field come from the lesson; the framing is ours.
13. Why can a schema with `maxItems: 3` still return an array of five items from some providers?Explain
A schema field categories with maxItems 3, and a reply whose categories array has five strings, with a question mark between them.
Schema field typed from the stem; the five values in the reply are illustrative.
14. Your schema constrains `status` to `in_stock`, `out_of_stock`, or `preorder`. A prompt-only approach returns `"available"`. Which failure mode is this?Analyze
A status field limited to in_stock, out_of_stock or preorder, and a prompt-only reply with status available. A question mark asks which failure mode it is.
Field and reply as in the question; typed.
15. A pipeline using JSON mode parses every response, yet it sometimes crashes with a missing `price` key. What explains this?Analyze
A pipeline: an API call in JSON mode, json.loads succeeding, then data[price] raising KeyError price. A question mark asks why a parsed response still breaks the code.
Schematic of the situation in the question.
16. Extraction on a five-level nested schema makes more structural mistakes than on a flat one. What is the lesson's explanation?Analyze
Two outlines. A flat schema with keys a to d at one level, and a schema nested five levels deep with keys a through e. A question mark asks the reason for more mistakes on the deeper one.
Generic key names; schematic.
17. You have ten extraction schemas (invoice, receipt, contract and so on) and each incoming document must reach the right one automatically. Which design fits?Design
Incoming documents invoice, receipt and contract flow into a dashed box marked with a question mark, which leads to invoice, receipt and contract schemas, with seven more making ten in all.
Schematic of the situation in the question.
18. You self-host an open model and need every response valid under your JSON Schema, with no retries. What approach fits?Design
A requirements card: an open model you host yourself, every response valid under your JSON Schema, no retries. A question mark asks which approach fits.
Typed summary of the question's requirements.

Build It

Step 1: JSON Schema Validator

Build a validator from scratch that checks whether a Python object matches a JSON Schema. This is what runs on the output side to verify compliance.

python
import json
 
def validate_schema(data, schema):
    errors = []
    _validate(data, schema, "", errors)
    return errors
 
def _validate(data, schema, path, errors):
    schema_type = schema.get("type")
 
    if schema_type == "object":
        if not isinstance(data, dict):
            errors.append(f"{path}: expected object, got {type(data).__name__}")
            return
        for key in schema.get("required", []):
            if key not in data:
                errors.append(f"{path}.{key}: required field missing")
        properties = schema.get("properties", {})
        for key, value in data.items():
            if key in properties:
                _validate(value, properties[key], f"{path}.{key}", errors)
 
    elif schema_type == "array":
        if not isinstance(data, list):
            errors.append(f"{path}: expected array, got {type(data).__name__}")
            return
        min_items = schema.get("minItems", 0)
        max_items = schema.get("maxItems", float("inf"))
        if len(data) < min_items:
            errors.append(f"{path}: array has {len(data)} items, minimum is {min_items}")
        if len(data) > max_items:
            errors.append(f"{path}: array has {len(data)} items, maximum is {max_items}")
        items_schema = schema.get("items", {})
        for i, item in enumerate(data):
            _validate(item, items_schema, f"{path}[{i}]", errors)
 
    elif schema_type == "string":
        if not isinstance(data, str):
            errors.append(f"{path}: expected string, got {type(data).__name__}")
            return
        enum_values = schema.get("enum")
        if enum_values and data not in enum_values:
            errors.append(f"{path}: '{data}' not in allowed values {enum_values}")
 
    elif schema_type == "number":
        if not isinstance(data, (int, float)):
            errors.append(f"{path}: expected number, got {type(data).__name__}")
            return
        minimum = schema.get("minimum")
        maximum = schema.get("maximum")
        if minimum is not None and data < minimum:
            errors.append(f"{path}: {data} is less than minimum {minimum}")
        if maximum is not None and data > maximum:
            errors.append(f"{path}: {data} is greater than maximum {maximum}")
 
    elif schema_type == "boolean":
        if not isinstance(data, bool):
            errors.append(f"{path}: expected boolean, got {type(data).__name__}")
 
    elif schema_type == "integer":
        if not isinstance(data, int) or isinstance(data, bool):
            errors.append(f"{path}: expected integer, got {type(data).__name__}")

Step 2: Pydantic-Style Model to Schema

Build a minimal class-to-schema converter. Define a Python class and generate its JSON Schema automatically.

python
class SchemaField:
    def __init__(self, field_type, required=True, default=None, enum=None, minimum=None, maximum=None):
        self.field_type = field_type
        self.required = required
        self.default = default
        self.enum = enum
        self.minimum = minimum
        self.maximum = maximum
 
def python_type_to_schema(field):
    type_map = {
        str: "string",
        int: "integer",
        float: "number",
        bool: "boolean",
    }
 
    schema = {}
 
    if field.field_type in type_map:
        schema["type"] = type_map[field.field_type]
    elif field.field_type == list:
        schema["type"] = "array"
        schema["items"] = {"type": "string"}
    elif isinstance(field.field_type, dict):
        schema = field.field_type
 
    if field.enum:
        schema["enum"] = field.enum
    if field.minimum is not None:
        schema["minimum"] = field.minimum
    if field.maximum is not None:
        schema["maximum"] = field.maximum
 
    return schema
 
def model_to_schema(name, fields):
    properties = {}
    required = []
 
    for field_name, field in fields.items():
        properties[field_name] = python_type_to_schema(field)
        if field.required:
            required.append(field_name)
 
    return {
        "type": "object",
        "properties": properties,
        "required": required,
    }

Step 3: Constrained Token Filter

Simulate constrained decoding. Given a partial JSON string and a schema, determine which token categories are valid at the current position.

python
def next_valid_tokens(partial_json, schema):
    stripped = partial_json.strip()
 
    if not stripped:
        return ["{"]
 
    try:
        json.loads(stripped)
        return ["<EOS>"]
    except json.JSONDecodeError:
        pass
 
    last_char = stripped[-1] if stripped else ""
 
    if last_char == "{":
        return ['"', "}"]
    elif last_char == '"':
        if stripped.endswith('":'):
            return ['"', "0-9", "true", "false", "null", "[", "{"]
        return ["a-z", '"']
    elif last_char == ":":
        return [" ", '"', "0-9", "true", "false", "null", "[", "{"]
    elif last_char == ",":
        return [" ", '"', "{", "["]
    elif last_char in "0123456789":
        return ["0-9", ".", ",", "}", "]"]
    elif last_char == "}":
        return [",", "}", "]", "<EOS>"]
    elif last_char == "]":
        return [",", "}", "<EOS>"]
    elif last_char == "[":
        return ['"', "0-9", "true", "false", "null", "{", "[", "]"]
    else:
        return ["any"]
 
def demonstrate_constrained_decoding():
    partial_states = [
        '',
        '{',
        '{"product"',
        '{"product":',
        '{"product": "Sony"',
        '{"product": "Sony",',
        '{"product": "Sony", "price":',
        '{"product": "Sony", "price": 348',
        '{"product": "Sony", "price": 348}',
    ]
 
    print(f"{'Partial JSON':<45} {'Valid Next Tokens'}")
    print("-" * 80)
    for state in partial_states:
        valid = next_valid_tokens(state, {})
        display = state if state else "(empty)"
        print(f"{display:<45} {valid}")

Step 4: Extraction Pipeline

Combine everything into an extraction pipeline: define a schema, simulate an LLM producing structured output, validate the output, and handle retries.

python
def simulate_llm_extraction(text, schema, attempt=0):
    if "headphones" in text.lower() or "sony" in text.lower():
        if attempt == 0:
            return '{"product": "Sony WH-1000XM5", "price": 348.00, "in_stock": true, "categories": ["audio", "headphones"]}'
        return '{"product": "Sony WH-1000XM5", "price": 348.00, "in_stock": true}'
 
    if "laptop" in text.lower():
        return '{"product": "MacBook Pro 16", "price": 2499.00, "in_stock": false, "categories": ["computers"]}'
 
    return '{"product": "Unknown", "price": 0, "in_stock": false}'
 
def extract_with_retry(text, schema, max_retries=3):
    for attempt in range(max_retries):
        raw = simulate_llm_extraction(text, schema, attempt)
 
        try:
            data = json.loads(raw)
        except json.JSONDecodeError as e:
            print(f"  Attempt {attempt + 1}: JSON parse error -- {e}")
            continue
 
        errors = validate_schema(data, schema)
        if not errors:
            return data
 
        print(f"  Attempt {attempt + 1}: Schema validation errors -- {errors}")
 
    return None
 
product_schema = {
    "type": "object",
    "properties": {
        "product": {"type": "string"},
        "price": {"type": "number", "minimum": 0},
        "in_stock": {"type": "boolean"},
        "categories": {"type": "array", "items": {"type": "string"}},
    },
    "required": ["product", "price", "in_stock"],
}

Step 5: Run the Full Pipeline

python
def run_demo():
    print("=" * 60)
    print("  Structured Output Pipeline Demo")
    print("=" * 60)
 
    print("\n--- Schema Definition ---")
    product_fields = {
        "product": SchemaField(str),
        "price": SchemaField(float, minimum=0),
        "in_stock": SchemaField(bool),
        "categories": SchemaField(list, required=False),
    }
    generated_schema = model_to_schema("Product", product_fields)
    print(json.dumps(generated_schema, indent=2))
 
    print("\n--- Schema Validation ---")
    test_cases = [
        ({"product": "Test", "price": 10.0, "in_stock": True}, "Valid object"),
        ({"product": "Test", "price": -5.0, "in_stock": True}, "Negative price"),
        ({"product": "Test", "in_stock": True}, "Missing price"),
        ({"product": "Test", "price": "ten", "in_stock": True}, "String as price"),
        ("not an object", "String instead of object"),
    ]
 
    for data, label in test_cases:
        errors = validate_schema(data, product_schema)
        status = "PASS" if not errors else f"FAIL: {errors}"
        print(f"  {label}: {status}")
 
    print("\n--- Constrained Decoding Simulation ---")
    demonstrate_constrained_decoding()
 
    print("\n--- Extraction Pipeline ---")
    texts = [
        "The Sony WH-1000XM5 headphones are priced at $348 and currently available.",
        "The new MacBook Pro 16-inch laptop costs $2499 but is sold out.",
        "This is a random sentence with no product info.",
    ]
 
    for text in texts:
        print(f"\n  Input: {text[:60]}...")
        result = extract_with_retry(text, product_schema)
        if result:
            print(f"  Output: {json.dumps(result)}")
        else:
            print(f"  Output: FAILED after retries")
Check: Build It0/13
1. A decoder has produced `{"price":` and the schema says price is a number. Which token does constrained decoding rule out?Apply
A schema line for price as a number, the text generated so far, and an arrow to a vocabulary box of 100K+ possible next tokens with a question mark.
Schema line trimmed from the lesson; the generated text is the one in the question.
2. What does `validate_schema({"product": "Test", "in_stock": True}, product_schema)` return?Apply
The lesson's product schema with properties product, price, in_stock and categories, its required list shown only as an ellipsis, then a call to validate_schema with an object containing product Test and in_stock True. A question mark asks what it returns.
Schema copied from the lesson with the required list elided; the call is the one in the question.
3. What does `validate_schema({"product": "Test", "price": "ten", "in_stock": True}, product_schema)` report?Apply
The lesson's product schema, then a call to validate_schema with product Test, price the string ten and in_stock True. A question mark asks what it reports.
Schema copied from the lesson; the call is the one in the question.
4. What does `validate_schema("not an object", product_schema)` return?Apply
The lesson's product schema, then a call validate_schema with the string not an object. A question mark asks what it returns.
Schema copied from the lesson; the call is the one in the question.
5. The validator is given `{"product": "A", "price": 5, "in_stock": True, "color": "red"}`. What does it return?Apply
The lesson's product schema, then a call to validate_schema with an object that has product A, price 5, in_stock True and an extra color key red. A question mark asks what it returns.
Schema copied from the lesson; the call is the one in the question.
6. What does `validate_schema(True, {"type": "integer"})` return?Apply
The call validate_schema with True and the schema type integer, and a question mark asking what it returns.
The call is the one in the question.
7. `next_valid_tokens('{"product": "Sony", "price": 348')` is called. What does it return?Apply
A call to next_valid_tokens with the partial text product Sony, price 348, shown with a cursor after the 8. A question mark asks what it returns.
The call is the one in the question.
8. `extract_with_retry` runs on "The Sony WH-1000XM5 headphones are priced at $348..." with `product_schema`. What is returned?Apply
A call to extract_with_retry on the Sony WH-1000XM5 headphones sentence, followed by four boxes: simulate, json.loads, validate_schema and return data. A question mark asks what it returns.
Sentence copied from the lesson's pipeline demo.
9. `extract_with_retry` runs on "This is a random sentence with no product info." What does it return?Apply
A call to extract_with_retry on the sentence This is a random sentence with no product info, followed by four boxes: simulate, json.loads, validate_schema and return data. A question mark asks what it returns.
The sentence is the one in the question.
10. `model_to_schema` is called with `SchemaField(list, required=False)` for `categories`. What does the result contain for that field?Apply
A fields dictionary with product, price, in_stock and categories as a list that is not required, passed to model_to_schema. A question mark asks what the result contains for categories.
Fields as in the lesson's schema generation demo, without rating.
11. The random-sentence input yields `{"product": "Unknown", "price": 0, ...}` and downstream code treats it as a real product. What is the design flaw?Analyze
A sentence with no product information, the output product Unknown with price 0 and in_stock false, and a box saying downstream code treats it as a real product. A question mark asks for the design flaw.
Input and output taken from the lesson's pipeline demo.
12. Retry design A calls the model again with only a higher attempt number. Design B includes the previous validation errors. Which is better and why?Analyze
Two prompt boxes for a second attempt. Design A has the extraction request and attempt 2. Design B has the extraction request plus the previous validation error price expected number got str. A question mark asks which is better.
Typed illustration; prompt wording is illustrative.
13. You ship an extractor on schema-enforced output. What combination best protects an inventory system from bad data?Design
A pipeline: source text, an extractor with schema-enforced output, a dashed box with a question mark, then the inventory system.
Schematic of the situation in the question.

Use It

OpenAI Structured Outputs

python
# from openai import OpenAI
# from pydantic import BaseModel
#
# client = OpenAI()
#
# class Product(BaseModel):
#     product: str
#     price: float
#     in_stock: bool
#
# response = client.beta.chat.completions.parse(
#     model="gpt-5-mini",
#     messages=[
#         {"role": "system", "content": "Extract product information."},
#         {"role": "user", "content": "Sony WH-1000XM5, $348, in stock"},
#     ],
#     response_format=Product,
# )
#
# product = response.choices[0].message.parsed
# print(product.product, product.price, product.in_stock)

OpenAI's structured output mode uses constrained decoding internally. Every token the model generates is guaranteed to produce output matching the Pydantic schema, so you need no retries and no validation. The constraint is part of the decoding process itself.

Anthropic Tool Use

python
# import anthropic
#
# client = anthropic.Anthropic()
#
# response = client.messages.create(
#     model="claude-opus-4-7",
#     max_tokens=1024,
#     tools=[{
#         "name": "extract_product",
#         "description": "Extract product information from text",
#         "input_schema": {
#             "type": "object",
#             "properties": {
#                 "product": {"type": "string"},
#                 "price": {"type": "number"},
#                 "in_stock": {"type": "boolean"},
#             },
#             "required": ["product", "price", "in_stock"],
#         },
#     }],
#     messages=[{"role": "user", "content": "Extract: Sony WH-1000XM5, $348, in stock"}],
# )

Anthropic gets structured output through tool use. The model emits a tool call whose arguments follow the input_schema. The result is the same with a different API surface.

Instructor Library

python
# pip install instructor
# import instructor
# from openai import OpenAI
# from pydantic import BaseModel
#
# client = instructor.from_openai(OpenAI())
#
# class Product(BaseModel):
#     product: str
#     price: float
#     in_stock: bool
#
# product = client.chat.completions.create(
#     model="gpt-5-mini",
#     response_model=Product,
#     messages=[{"role": "user", "content": "Sony WH-1000XM5, $348, in stock"}],
# )

Instructor wraps any LLM client and adds automatic retries with validation. If the first attempt fails validation, it sends the errors back to the model as context and asks it to fix the output. It works with any provider, not just OpenAI.

Check: Use It0/3
1. What is the Instructor library?Define
Python code that creates a client with instructor.from_openai, defines a Product class, and calls client.chat.completions.create with response_model Product.
Condensed from the lesson's Use It snippet.
2. What is a retry loop in structured extraction?Define
A model output with price ten as a string, and a dashed box asking what goes into attempt 2.
Output typed from the lesson's test case.
3. In the OpenAI example, where do you read the result after `client.beta.chat.completions.parse(...)`?Apply
The OpenAI parse call with response_format Product, followed by a line where the path after response is blank, and a print of product.product, product.price and product.in_stock.
Condensed from the lesson's Use It snippet; the blank is ours.

Ship It

This lesson produces outputs/prompt-structured-extractor.md as a reusable prompt template that extracts structured data from text given a schema definition. Feed it a JSON Schema and unstructured text and it returns validated JSON.

It also produces outputs/skill-structured-outputs.md as a decision framework for choosing a structured output strategy based on your provider, reliability requirements, and schema complexity.

Author's Note

I'd run main.py first and read the output top to bottom. It runs five demos in order: the validator against eight hand-built cases, schema generation from SchemaField definitions, the token filter table, the extraction pipeline on four texts, and a nested order schema with a valid and an invalid example. The failing cases print the exact error path, which is the quickest way to see what the validator does and doesn't check.

The constrained decoding step is the one to read skeptically. next_valid_tokens takes a schema argument and never looks at it. It only inspects the last character of the partial JSON, so it shows the shape of token masking and not schema-aware masking. A real engine tracks grammar state and masks the whole vocabulary against it.

The retry loop has the same kind of gap. extract_with_retry calls the simulated model again with a higher attempt number and does not pass the validation errors back, which is the part that makes Instructor's retries useful. I'd add that before trusting the pipeline shape. Everything the "LLM" returns is canned, so the pipeline demo shows the plumbing and tells you nothing about extraction accuracy.

Exercises

  1. Extend the schema validator to support oneOf (the data must match exactly one of several schemas). This handles polymorphic outputs, for example a field that can be either a Product or a Service object with different shapes.

  2. Build a "schema diff" tool that compares two schemas and identifies breaking changes (removed required fields, changed types) versus non-breaking changes (added optional fields, relaxed constraints). You need this to version extraction schemas in production.

  3. Implement a more realistic constrained decoding simulator. Given a JSON Schema and a vocabulary of 100 tokens (letters, digits, punctuation, keywords), walk through generation step by step, masking invalid tokens at each position. Measure what percentage of the vocabulary is valid at each step.

  4. Build an extraction eval suite. Create 50 product descriptions with hand-labeled JSON outputs. Run your extraction pipeline on all 50 and measure exact match, field-level accuracy, and type compliance. Identify which fields are hardest to extract correctly.

  5. Add "confidence scores" to your extraction pipeline. For each extracted field, estimate how confident the model is (based on token probabilities, or by running extraction 3 times and measuring consistency). Flag low-confidence fields for human review.

Key Terms

TermWhat people sayWhat it actually means
JSON mode"Returns JSON"API flag that guarantees syntactically valid JSON output, but does not enforce any particular schema
Structured output"Typed JSON"Output that matches a specific JSON Schema with correct keys, types, and constraints
Constrained decoding"Guided generation"At each token position, mask out tokens that would produce invalid output -- guarantees 100% schema compliance
JSON Schema"A JSON template"A declarative language for describing the structure, types, and constraints of JSON data (used by OpenAPI, JSON Forms, etc.)
Pydantic"Python dataclasses+"Python library that defines data models with type validation, used by FastAPI and Instructor to generate JSON Schemas
Function calling"Tool use"LLM outputs a structured function invocation (name + typed arguments) instead of free text -- OpenAI and Anthropic both support this
Instructor"Pydantic for LLMs"Python library that wraps LLM clients to return validated Pydantic instances, with automatic retry on validation failure
Token masking"Filtering the vocabulary"Setting specific token probabilities to zero during generation so the model cannot produce them
Schema compliance"Matches the shape"The output has every required field, correct types, values within constraints, and no extra disallowed fields
Retry loop"Try again until it works"Send validation errors back to the model and ask it to fix the output -- Instructor does this automatically, up to a configurable max

Further Reading

Code

Ships With