← Back to Learning Hub

Structured Outputs

Structured outFew-shotIntermediate21 min

By: Anacodic Team

TL;DR — There are two different things people call "getting JSON out of a model", and they are not on the same scale. Asking — a prompt, examples, a schema pasted in — makes invalid output unlikely. Constraining — masking any next token that would break the grammar — makes it impossible. The measured difference below: asking fails 14.65% of the time at T=1.0 and still 0.10% at T=0.3, while constrained decoding fails 0.00%, because the invalid branches are unreachable rather than merely improbable. A rare failure is also worse to live with than a common one, since it's harder to reproduce. The limit worth stating up front: constraining guarantees the shape, never the value — in the same run, 72% of the perfectly valid outputs said true, and nothing here has an opinion about whether that was correct.


1. Simple explanation

You want a model's output to go straight into code — a JSON object, a specific set of fields, the right types. Free text won't do.

The obvious approach is to ask: describe the format, show examples, maybe paste in the schema. This works most of the time, which is the problem. "Most of the time" means your parser throws occasionally, at a rate low enough to look like noise and high enough to fill an error queue.

The other approach changes the mechanism. Since the model produces a probability for every possible next token, you can look at the partially-generated output, work out which tokens would make it invalid, and set those probabilities to zero before one is chosen. The model cannot emit a malformed field name because that path no longer exists.

Analogy — asking someone to fill in a form versus giving them a form. You can hand over a blank sheet with careful written instructions about which fields to include and in what order. Most people comply; some add a covering note, some reorder things, one writes a paragraph. Or you hand them a form with boxes: name here, date here. The second isn't "clearer instructions" — it's a different kind of thing, because non-compliance is no longer available. The analogy carries the limit too: a correctly filled form can still contain the wrong date.


2. Diagram

TWO MECHANISMS, NOT TWO EFFORT LEVELS

  ASKING                              CONSTRAINING
  ───────────────────────────         ────────────────────────────
  prompt: "reply with JSON"           at each step, the grammar says
  + examples                          which tokens are legal NEXT
  + schema in the text                          │
          │                                     ▼
          ▼                           mask the illegal ones to zero
  model emits whatever                          │
  it finds most likely                          ▼
          │                           sample only from what remains
          ▼                                     │
  hope it parses                                ▼
                                      cannot produce invalid output


MEASURED — same distribution, 4000 draws

  setting                 % invalid
  T=1.0 (as produced)        14.65%   ████████████████
  T=0.7                       5.12%   █████
  T=0.3                       0.10%   ▏          ← rare, NOT zero
  T=0.0 (greedy)              0.00%              ← but no variation at all
  constrained                 0.00%   ← and full variation retained


WHAT THE MASK DOES, ONE TOKEN AT A TIME

  produced so far:  {"ok":
  legal next:       true | false                 ← grammar says boolean
  model wanted:     true(0.62) True(0.02) yes(0.01) ...
  after masking:    true(0.97) false(0.03)
                          ▲
                'True' was never an option, so the
                Python-literal failure cannot occur


AND THE LIMIT

  constrained output:  {"ok": true}   ← always parses  ✅
                              ▲
                        is that the RIGHT answer?
                        Nothing here can tell you.
                        72% of valid outputs said true.

3. How it works

3.1 Why asking plateaus

Every malformed output you've seen — a code fence, a "Sure!" preamble, True instead of true, an unquoted key — is a token sequence with non-zero probability. Prompting pushes those probabilities down. It cannot push them to zero.

So the failure rate falls as you improve the prompt or lower the temperature, and it asymptotes above zero. §4 shows the curve: 14.65% → 5.12% → 0.10%. Each step feels like progress and none of them arrives.

Worse, a rare failure is harder to work with than a common one. At 15% you reproduce it immediately; at 0.1% it appears in production, resists reproduction, and gets closed as "transient".

3.2 What constraining actually does

The model still produces a distribution over every token. Constrained decoding inserts a step between that and the choice:

  1. Track what has been generated so far.
  2. Ask a grammar or state machine which tokens are legal next.
  3. Set every illegal token's probability to zero and renormalise.
  4. Sample from what remains.

After {"ok": a JSON-boolean grammar permits true and false and nothing else. True isn't unlikely — it is absent. That's the entire difference.

This runs per token, so a valid document is built incrementally with no possibility of drift. It's usually exposed as a "JSON mode", a "response format" parameter, or a schema you supply; under the hood it's token masking.

3.3 Function calling is the same mechanism

Tool use — describing functions and having the model choose one with arguments — is structured output wearing a different name. The tool definition is a schema, the model's call is a constrained generation against it, and you get a validated object rather than prose to parse.

Which is why the two topics share failure modes: an under-specified schema produces plausible-but-useless arguments in exactly the way an under-specified output schema produces plausible-but-useless fields. See Tool Use and MCP Fundamentals.

3.4 What it costs you

Constraining is not free:

costdetail
Availabilitynot every model or provider supports it; some support only a JSON subset
Schema rigidityrecursive or highly dynamic schemas are often unsupported or slow
Quality interactionforcing a shape the model finds unnatural can degrade the content; a model that would have said "I'm not sure" may be forced into a confident-looking field
Latencymasking adds per-token work, usually small but non-zero
False securityvalid shape reads as validated data, and it isn't

That third row deserves attention. If your schema has no way to express uncertainty, you have removed the model's ability to signal it. Give the schema a null, a confidence, or an explicit unknown — otherwise you've converted "I don't know" into a fabricated value, which is a worse outcome than a parse error.

3.5 Shape is not correctness

The measured run makes this concrete: constrained output parses 100% of the time, and 72% of those valid outputs said true. Whether true was the right answer is a completely separate question that no grammar can address.

So structured outputs remove one class of bug — malformed responses — and leave the more important one untouched. You still need the value checked: against a retrieved source (Grounding and Abstention by Design: Refusing Before You Generate the Wrong Answer), against a range or an enum, or against a scored evaluation set.

Valid JSON is a floor, not a guarantee.

3.6 Where to reach for something else

If your provider doesn't support constraining, the fallback ladder is: ask well (schema in the prompt plus consistent examples), validate on receipt, and retry once with the validation error fed back. That converges in practice and costs up to double on the retry path — acceptable, but note it's recovery, not prevention.

And if the output isn't really structured — prose, a summary, an explanation — forcing a schema onto it fights the model for no benefit. Structured outputs are for data.


4. The math

4.1 Asking versus constraining

  asking:
      P(invalid) = Σ p(t) over all invalid token sequences t
                 > 0   for any temperature above 0

  constraining:
      p'(t) = p(t) / Σ p(valid)   if t is legal
            = 0                    otherwise
      P(invalid) = 0   exactly, by construction

4.2 The retry ladder, when constraining is unavailable

  with per-call failure rate f and one retry:
      P(fail after retry) = f²
      expected calls      = 1 + f

  f = 0.05  ->  0.25% residual failure, 1.05 calls average

Better, still not zero — and you pay for it on the retry path.

4.3 Worked example

A distribution over seven candidate completions for a yes/no field. Two are valid JSON matching the schema; five are the failures people actually see — a code fence, a preamble, a Python literal, an unquoted key, plain prose.

Asking, at four temperatures:

ASKING for JSON -- failure rate by temperature
  setting                 % invalid
  T=1.0 (as produced)        14.65%
  T=0.7                       5.12%
  T=0.3                       0.10%
  T=0.0 (greedy)              0.00%

Lowering temperature works, right up until you notice what it costs. T=0.3 is at 0.10% — one call in a thousand, which will still fill a queue at volume and will not reproduce on demand. T=0.0 reaches zero here only because the top candidate happens to be valid; it also eliminates all variation, which is fine for extraction and useless if you wanted variety.

Constraining:

CONSTRAINING the output -- the invalid branches are unreachable
  candidates before 7  ->  after 2
    0.721  {"ok": true}
    0.279  {"ok": false}
  constrained                 0.00%

Seven candidates become two. Both parse. The failure rate is 0.00% not because it's small but because there is no path to an invalid output.

The three approaches side by side:

  approach                  best case   guarantee?
  ask nicely (prompt)           0.10%           no
  ask + retry on error             ~0  no (costs 2x)
  constrained decoding          0.00%          YES

The middle row is the honest fallback: it gets close, and it is recovery rather than prevention, at up to double the calls.

4.4 And the limit

  ...but 72% of valid outputs say ok=true.

Every constrained output is well-formed. 72% of them assert true. Whether that is correct is outside what any of this can determine.


5. Real code

"""Asking for JSON vs constraining it: unlikely-invalid against impossible-invalid."""
import json
import random

# What a model might produce for "return {"ok": true} or {"ok": false}".
# Only the first two are valid; the rest are the failures people actually see.
# FENCE is built rather than written literally so this file can itself sit inside
# a markdown code block -- a literal triple backtick would close the fence early.
FENCE = "`" * 3
CANDIDATES = {
    '{"ok": true}':                       0.62,
    '{"ok": false}':                      0.24,
    f'{FENCE}json\n{{"ok": true}}\n{FENCE}': 0.06,   # fenced
    'Sure! {"ok": true}':                 0.04,   # preamble
    '{"ok": True}':                       0.02,   # Python literal, not JSON
    '{ok: true}':                         0.01,   # unquoted key
    'The answer is yes.':                 0.01,   # prose
}
VALID_KEYS = {"ok"}


def parses(text: str) -> bool:
    try:
        obj = json.loads(text)
    except Exception:
        return False
    return isinstance(obj, dict) and set(obj) == VALID_KEYS and isinstance(obj["ok"], bool)


def sample(dist, rng):
    r, acc = rng.random(), 0.0
    for t, p in dist.items():
        acc += p
        if r <= acc:
            return t
    return next(reversed(dist))


def rescale(dist, T):
    if T <= 0:
        return {max(dist, key=dist.get): 1.0}
    import math
    lg = {t: math.log(p) / T for t, p in dist.items()}
    m = max(lg.values())
    e = {t: math.exp(v - m) for t, v in lg.items()}
    z = sum(e.values())
    return {t: v / z for t, v in e.items()}


def constrain(dist):
    """Mask every candidate the grammar forbids, then renormalise.
    This is what constrained decoding does, one token at a time."""
    kept = {t: p for t, p in dist.items() if parses(t)}
    z = sum(kept.values())
    return {t: p / z for t, p in kept.items()}


def trial(dist, n=4000, seed=0):
    rng = random.Random(seed)
    outs = [sample(dist, rng) for _ in range(n)]
    return sum(1 for o in outs if not parses(o)) / n * 100


print("ASKING for JSON -- failure rate by temperature")
print(f"  {'setting':<22} {'% invalid':>10}")
for label, d in [("T=1.0 (as produced)", CANDIDATES),
                 ("T=0.7", rescale(CANDIDATES, 0.7)),
                 ("T=0.3", rescale(CANDIDATES, 0.3)),
                 ("T=0.0 (greedy)", rescale(CANDIDATES, 0.0))]:
    print(f"  {label:<22} {trial(d):>9.2f}%")

print("\n  Lowering temperature makes the failure RARER and never zero --")
print("  except at T=0, which also removes all variation. And a rare")
print("  failure is harder to reproduce, so it is worse to debug.")

print("\nCONSTRAINING the output -- the invalid branches are unreachable")
c = constrain(CANDIDATES)
print(f"  candidates before {len(CANDIDATES)}  ->  after {len(c)}")
for t, p in c.items():
    print(f"    {p:.3f}  {t}")
print(f"  {'constrained':<22} {trial(c):>9.2f}%")

print("\nWHY IT IS DIFFERENT (not just 'better')")
print(f"  {'approach':<24} {'best case':>10}  {'guarantee?':>11}")
print(f"  {'ask nicely (prompt)':<24} {trial(rescale(CANDIDATES, 0.3)):>9.2f}%  {'no':>11}")
print(f"  {'ask + retry on error':<24} {'~0':>10}  {'no (costs 2x)':>11}")
print(f"  {'constrained decoding':<24} {trial(c):>9.2f}%  {'YES':>11}")

# A schema is not the same as a correct answer.
truthy = sum(p for t, p in c.items() if json.loads(t)["ok"] is True)
print(f"\n  ...but {truthy:.0%} of valid outputs say ok=true. Constraining makes the")
print("  SHAPE certain. It says nothing about whether the VALUE is right.")

assert trial(CANDIDATES) > 10
assert trial(rescale(CANDIDATES, 0.3)) > 0        # still fails, just less often
assert trial(rescale(CANDIDATES, 0.3)) < trial(CANDIDATES)
assert trial(c) == 0.0                            # impossible, not improbable
assert len(c) == 2 and all(parses(t) for t in c)
assert abs(sum(c.values()) - 1.0) < 1e-9
print("\nall assertions passed")

# Output:
#   ASKING for JSON -- failure rate by temperature
#     setting                 % invalid
#     T=1.0 (as produced)        14.65%
#     T=0.7                       5.12%
#     T=0.3                       0.10%
#     T=0.0 (greedy)              0.00%
#
#     Lowering temperature makes the failure RARER and never zero --
#     except at T=0, which also removes all variation. And a rare
#     failure is harder to reproduce, so it is worse to debug.
#
#   CONSTRAINING the output -- the invalid branches are unreachable
#     candidates before 7  ->  after 2
#       0.721  {"ok": true}
#       0.279  {"ok": false}
#     constrained                 0.00%
#
#   WHY IT IS DIFFERENT (not just 'better')
#     approach                  best case   guarantee?
#     ask nicely (prompt)           0.10%           no
#     ask + retry on error             ~0  no (costs 2x)
#     constrained decoding          0.00%          YES
#
#     ...but 72% of valid outputs say ok=true. Constraining makes the
#     SHAPE certain. It says nothing about whether the VALUE is right.
#
#   all assertions passed

constrain masks whole candidate strings so the idea fits on a page; a real implementation masks one token at a time against a grammar or a compiled state machine. The effect is identical — illegal continuations get probability zero — and doing it per token is what lets it build arbitrarily long valid documents.


6. Real-world example

A team extracted structured fields from documents with a prompt containing the schema and three examples. Parse failures ran at about 2%, handled by a retry, and everyone considered it solved.

Then they added a field for an optional review date. The schema said the key was required and the value could be a date string or null.

Extraction accuracy on that field was poor in a specific way: when no date was present, the model returned plausible-looking dates rather than null. Roughly one in seven absent dates came back as a confident fabrication, and downstream systems scheduled reviews that had never been agreed.

Two causes, and both are §3.4. None of the three examples showed the null case — every example had a date — so the model had never seen the absent case demonstrated. And when they later moved to a strict JSON mode, they made the field "type": "string" rather than allowing null, which removed the model's only way to say "not present". Constraining then guaranteed it produced a string, and the only strings available were invented ones.

The fix had two parts: allow null in the schema, and add an example showing it. Fabrications went to near zero.

The lesson generalises past JSON. A schema that cannot express uncertainty converts "I don't know" into a fabrication, and constrained decoding enforces that conversion rigorously. Always give the shape somewhere to put "absent", "unknown", or "not confident" — otherwise you have used a correctness tool to guarantee a wrong answer.


7. Interview questions companies actually ask

Q1. What's the difference between asking for JSON and constrained decoding? Asking lowers the probability of invalid output; constraining removes the possibility. Every malformed response — a code fence, a preamble, True instead of true — is a token sequence with non-zero probability, and prompting cannot drive that to zero. Constrained decoding masks illegal tokens before sampling, so the invalid branches don't exist. Measured: 0.10% versus 0.00%.

Q2. Why not just set temperature to 0? It removes variation, not invalidity. It reached zero in the example only because the top candidate happened to be valid — with a malformed top candidate you get that malformed output every single time, reliably. And you lose all variety, which matters if the task wanted any.

Q3. How does constrained decoding actually work? At each step the model produces a distribution over all tokens. A grammar or state machine, tracking what's been generated, determines which tokens are legal next; the rest have their probability set to zero and the remainder is renormalised. After {"ok": a JSON grammar allows true and false and nothing else, so True isn't unlikely — it's absent.

Q4. What does constraining not give you? Correctness. It guarantees the shape and says nothing about the values. In the worked example every constrained output parsed and 72% said true, and no grammar can tell you whether that was right. You still need the value checked against a source, a range, or a scored evaluation set.

Q5. When would you avoid it? When the provider doesn't support it, when the schema is recursive or highly dynamic, when the output genuinely isn't structured data, and when forcing a shape degrades the content — a model that would say "I'm not sure" being forced into a confident field. Fallback: schema in the prompt, consistent examples, validate on receipt, retry once with the error fed back.

Q6. Your model returns valid JSON with fabricated values. What went wrong? Almost certainly the schema has no way to express absence, and possibly no example shows it either. If a field is a required string with no null allowed, you have removed the model's only way to signal "not present" — and constraining then enforces that a string is produced, so the only strings available are invented. Allow null, and demonstrate it in an example.

Q7. How is function calling related? It's the same mechanism. A tool definition is a schema, the model's call is a constrained generation against it, and you get a validated object instead of prose to parse. Which is why they share failure modes: an under-specified tool schema produces plausible-but-useless arguments exactly as an under-specified output schema produces plausible-but-useless fields.


8. When to use / tradeoffs

Use constrained decoding when:

  • Output feeds code directly — extraction, classification, tool arguments
  • A parse failure has real cost
  • The schema is fixed and reasonably simple
  • Your provider supports it

Use ask-and-validate when:

  • Constraining is unavailable
  • The schema is recursive or changes per request
  • You can absorb an occasional retry

Use neither when:

  • The output is prose, a summary, or an explanation
SituationWhy it breaksDo this instead
Lower the temperature to fix malformed JSONRarer, never zero — and harder to reproduceConstrain
T=0 for guaranteed validityOnly works if the top candidate is valid; kills varietyConstrain
Required string field, no nullRemoves the way to say "absent" → fabricationAllow null / unknown
Examples never show the empty caseModel guesses the conventionDemonstrate it
Valid JSON treated as validated dataShape ≠ correctnessCheck values separately
Recursive or dynamic schemaOften unsupported or slowAsk + validate + retry
Forcing a schema onto proseFights the model for no gainLeave it unstructured

Honest limits. The example masks whole candidate strings, where a real implementation masks per token against a compiled grammar — the effect is the same but the engineering is considerably more involved, and support for it varies widely across providers and schema features. The seven candidates and their probabilities are illustrative: your real failure mix and rate will differ, and the specific 14.65% is a property of this made-up distribution rather than a benchmark. The claim that constraining is exactly 0% assumes the grammar is correct and the implementation is sound; a buggy grammar produces confidently valid nonsense, which is not obviously better than a parse error. And §3.4's warning about quality degradation is real but hard to quantify — there's evidence in both directions on whether constraining hurts content, so measure it on your task rather than assuming either way.


  • Asking makes invalid output unlikely. Constraining makes it impossible. Different mechanisms, not different effort levels.
  • Measured: asking fails 14.65% at T=1.0, 0.10% at T=0.3; constrained fails 0.00% — because there is no path to an invalid output.
  • A rare failure is worse to live with than a common one: same queue, no reproduction.
  • T=0 reaches zero only when the top candidate happens to be valid, and it removes all variation.
  • Constraining works by masking illegal next tokens against a grammar, per token, then renormalising.
  • Function calling is the same mechanism with a different name, and shares the failure modes.
  • Shape is not correctness. Every constrained output parsed; 72% said true; nothing here knows if that's right.
  • A schema with no way to express absence converts "I don't know" into a fabrication — and constraining enforces it rigorously.
  • Fallback when unavailable: schema in the prompt, consistent examples, validate, retry once. Recovery, not prevention, at up to 2× calls.

Related:

Resources