Seekvana
Prompt Engineeringintermediate

Structured Outputs: Get JSON You Can Actually Trust

Structured outputs use schema-constrained decoding to guarantee valid JSON from an LLM, unlike JSON mode alone. Here's how to build one.

SeekvanaJuly 10, 20268 min read
Share
Editorial illustration of scattered text funneling down into a clean, labeled JSON block

A lead-capture pipeline pulls a name and email out of a chat transcript, drops it into a webhook, and moves on. It's worked for weeks. Then one call comes back with "full_name" instead of "name", the webhook doesn't recognize the field, and the lead just vanishes. Nothing crashed. Nothing logged an error. The model did exactly what it was asked: return the info as JSON. It just didn't promise the field would be called the same thing twice.

That's the gap structured outputs close. Asking a model to "respond in JSON" only guarantees valid syntax, not a specific shape. Structured outputs and schema-driven prompting mean handing the model an actual contract, a JSON Schema describing exact field names, types, and requirements, and having the model's response constrained at the token level so it cannot violate that contract.

In this lesson, you'll learn why plain JSON requests aren't reliable enough for production, the three levels of reliability between "asking nicely" and "guaranteed," and how to build and test a schema-constrained extraction yourself. This builds directly on controlling how a model formats its output: a schema is the strictest form of that control, the same way keeping RAG answers grounded is the strictest form of controlling what a model is even allowed to talk about.

Key Takeaways

  • Plain JSON mode guarantees valid syntax only, not a consistent shape, so field names and structure can shift between calls even when every response is technically valid JSON.
  • Structured outputs use constrained decoding, blocking any token that would break the schema, which is why Anthropic reports schema-compliant responses even under heavy production load.
  • Writing a schema is a form of prompting: you're specifying a contract instead of a sentence.
  • Even strict schemas aren't unbreakable: safety refusals and token limits can still produce output that doesn't match your shape.

What Is Schema-Driven Prompting?

Schema-driven prompting means describing the exact shape you want a model's output to take, field names, types, and which fields are required, and having that description enforced rather than just requested. It's still prompting. You're just writing the instruction as a structured contract instead of a sentence.

A JSON Schema is the format most providers use for this contract. It looks like this for a simple lead-extraction task:

{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "email": { "type": "string" },
    "wants_demo": { "type": "boolean" }
  },
  "required": ["name", "email", "wants_demo"],
  "additionalProperties": false
}

additionalProperties: false matters here. Without it, the model is free to add extra fields you never asked for, which quietly reintroduces the same inconsistency problem you were trying to avoid.

Why "Just Say JSON" Isn't Enough

Ask a model to "respond in JSON" and most providers will genuinely give you syntactically valid JSON back. The problem shows up in the shape, not the syntax.

Run the same extraction prompt five times without a schema. You can get "name" on one run and "full_name" on the next. An email might arrive as a plain string one time and nested inside a "contact" object the next. Nothing about that violates "respond in JSON." But it breaks any code downstream that expected a consistent structure.

That inconsistency isn't rare. As one detailed comparison of JSON mode, function calling, and structured outputs puts it, JSON mode "does guarantee that the output is valid JSON, but it does not guarantee a specific structure." At any real volume, that gap between valid and consistent is where pipelines quietly break.

Skip schema enforcement and the failure mode isn't a crash you can catch in testing. It's a slow, silent shape drift that shows up as missing data three weeks later.

Comparison showing plain JSON mode producing inconsistent field names and nesting versus structured outputs producing the same shape on every run
Plain JSON mode still lets fields drift between runs, structured outputs lock the shape so every run comes back identical.

The Three Levels of Structured Output Reliability

Getting structured data out of a model isn't one technique, it's a ladder. Each rung trades a bit more setup for a lot more guarantee.

Structured output reliability, low to high

LevelMethodHow it's enforcedReliability
1Prompt-only ("respond in JSON")Instruction only, no enforcementValid JSON, shape not guaranteed
2Function calling / tool useModel fills a defined tool schemaHigh, but not schema-guaranteed on every provider
3Native structured outputConstrained decoding blocks invalid tokensSchema-compliant by construction

Level 3, native structured output, works by compiling your schema into a grammar the decoder enforces at every token, not by hoping the model remembers the instruction. If the schema says a field is a boolean, the decoder simply never lets the model generate a token that isn't true or false in that position.

Level 2 and Level 3 aren't always separate steps. Anthropic's strict tool use and OpenAI's structured outputs both apply the same constrained-decoding idea to function calling, so a well-defined tool schema often gets you Level 3 guarantees for free.

Building a Schema-Constrained Extraction

Here's the same lead-extraction example, this time as an actual API call using Claude's structured outputs.

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": "Extract from: Jordan Lee (jordan@example.com) wants a demo."
    }],
    output_config={
        "format": {
            "type": "json_schema",
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                    "wants_demo": {"type": "boolean"}
                },
                "required": ["name", "email", "wants_demo"],
                "additionalProperties": False
            }
        }
    }
)

Run that ten times and you get the same three fields, same names, same types, every single time. No regex cleanup, no "hope it parsed" retry logic. The schema is the prompt for the shape, the message is the prompt for the content.

What Still Breaks (Edge Cases)

Schema enforcement is a huge reliability jump, not a guarantee against every failure. Three things still slip through even with strict mode enabled.

Safety refusals override schema enforcement. If a model decides a request shouldn't be answered, it returns a refusal instead of schema-shaped output, so your code still needs to check for that case explicitly. A token limit cutting a response short before the JSON closes is a second real failure mode, and enum values differing only in capitalization from what your schema expects is a third.

I've shipped a pipeline that passed every test case in staging and then failed in production the first time a real user's message tripped a refusal, because none of the test inputs ever needed one. Structured outputs remove the shape-drift failure, not every failure. Testing edge cases, not just the happy path, is what separates a demo from something you can trust.

Your Task

Write a schema for a real extraction

Pick something you'd actually want structured data from, an email, a support ticket, a product review, and write a JSON Schema with 3 to 5 fields, marking which ones are required.

Run it through a model with structured output enabled

Send your schema and one real example through Claude, GPT, or Gemini's structured output feature and confirm the response matches your schema exactly.

Break it on purpose

Feed the model an input that's ambiguous or missing information for one required field, and see what it does. Note whether it guesses, leaves the field oddly filled, or something else you didn't expect.

Done? You've completed Lesson 12.08 and built your own schema-constrained extraction. Next up: the next lesson in this module, coming soon →

FAQ

Common questions

  • No. JSON mode only guarantees the response is valid JSON, it says nothing about its shape. Structured outputs go further: they guarantee the response matches a specific schema you define, so every field you need is there, in the right type, every time.

  • No, those are conveniences, not requirements. You can hand-write a JSON Schema directly and pass it to the API. Pydantic and Zod just let you define that schema as a class in Python or TypeScript instead of raw JSON, which most developers find faster to read and maintain.

  • Safety refusals take priority over schema enforcement. If a model decides a request is unsafe to answer, it returns a refusal instead of schema-shaped output, no matter how strict your schema is. This is intentional: the schema constrains what a normal answer looks like, it doesn't override the model's other guardrails.

  • A little, yes. The provider adds extra system-prompt tokens explaining the format, which costs slightly more per call. Still, that small cost usually beats the alternative. Without a schema, enough calls come back in the wrong shape that you end up paying for retries and cleanup anyway, and that adds up to more tokens than paying the schema tax up front.

Finished reading?

Mark it complete to track your progress through the path.

Share this article

Was this article helpful?

Comments (0)

0/1000

Be the first to leave a comment.