~13 min
Ask a model to "respond only in JSON" and most of the time it will — until it adds a stray sentence before the braces, forgets a required field, or invents a value for an enum you never listed. Early attempts to fix this relied entirely on the prompt: stronger wording, more examples, occasional retries when parsing failed. That approach caps out, because nothing is actually constraining what tokens the model can produce next.
Structured outputs replace prompting with a guarantee. Instead of hoping the model's free-text response happens to parse, you hand the API a JSON Schema, and the API constrains token generation so only schema-valid continuations are possible — a technique usually called constrained or grammar-based decoding. The result isn't "probably valid JSON"; it's JSON that is syntactically guaranteed to match the shape you specified, every time the request completes normally.
python
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-6-astra",
messages=[
{"role": "user", "content": "Extract the contact from: John Smith, john@example.com, Enterprise plan."}
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "contact",
"strict": True,
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"plan": {"type": "string"},
},
"required": ["name", "email", "plan"],
"additionalProperties": False,
},
},
},
)
print(response.choices[0].message.content)OpenAI's newer Responses API defines this under text.format,
with type: "json_schema" and your schema attached; the older
Chat Completions endpoint uses response_format with the same
shape, and OpenAI's own migration guidance says plainly that
response_format moved to text.format in the newer API rather
than being replaced by something different. Both are distinct
from the older, looser json_object mode: json_object only
guarantees the output parses as JSON, with no check that it has
the fields, types, or enum values your schema demands.
Structured outputs, with strict: true on the schema, guarantee
both.
The Python and JavaScript SDKs let you define the schema from a
Pydantic model or Zod schema instead of hand-writing raw JSON
Schema, and a .parse() helper returns an already-validated
object. Under the hood it's the same constrained decoding either
way — the SDK is just saving you from writing the schema by
hand.
python
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[
{"role": "user", "content": "Extract the contact from: John Smith, john@example.com, Enterprise plan."}
],
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"plan": {"type": "string"},
},
"required": ["name", "email", "plan"],
"additionalProperties": False,
},
}
},
)
print(next(b.text for b in response.content if b.type == "text"))Anthropic's equivalent lives under output_config.format, again
type: "json_schema" plus your schema, using the same
constrained-sampling idea: your schema is compiled into a
grammar the token sampler can't leave. The first request against
a new schema pays a compilation cost; Anthropic's documentation
states compiled grammars are then cached for 24 hours from last
use, so repeated calls with the same schema shape are fast.
Two quirks are worth knowing before you rely on this in production. First, property ordering: Anthropic's structured outputs always emit required properties before optional ones, in schema order within each group — not necessarily the order you wrote them if you mixed required and optional fields, so don't assume your schema's raw property order survives into the response. Second, enum casing isn't fully guaranteed: the documentation notes a returned enum value can differ from your schema only in capitalization, so compare enum values case-insensitively rather than with strict string equality.
No — this is documented, expected behavior: a safety refusal takes precedence over the schema, and you're still billed for the tokens generated. Check for a refusal before assuming the response matches your schema.
Structured outputs and tool or function calling solve related
but different problems, and it's worth being precise about
which one you reach for. Use schema-enforced JSON output
(text.format / output_config.format) when the model is
producing its final answer to your application in a shape
you'll parse — a classification label, an extracted record, a
UI-ready object. Use tool calling, covered in the next module,
when the model needs to decide whether to take an action and
what arguments to pass — searching a database, calling a
function in your codebase. The two combine: a model can call a
tool with a schema-validated argument set and still return a
schema-validated final summary to the user in the same turn.