An LLM is a next-token predictor. It has no internal notion of "this brace must close three tokens from now," so every character of structure is a probabilistic guess. That is why prompting alone cannot guarantee output shape: the model may wrap JSON in markdown fences, add a chatty preamble, drop a required key, or return a string where you wanted a number (Pydantic's guide to LLM validation, constrained decoding mechanics).
Five failure modes cover most production incidents:
| Failure | Example | Typical cause |
|---|---|---|
| Truncation | {"name": "Alice", "age": 30 | Token limit, timeout, dropped stream |
| Markdown fence | ```json { ... } ``` | Model copies JSON-in-code-block pattern |
| Prose around JSON | Here is the data: { ... } | Instruction drift |
| Trailing comma | {"age": 35,} | Strict JSON forbids it, training data often allows it |
| Schema violation | {"age": "twenty-five"} | Right syntax, wrong type |
A worked catalogue of these appears in the constrained decoding handbook chapter, which notes that unconstrained generation produces no guarantees at the token level. The vLLM structured-output guide makes the same point practically: logit masking forces a legal token to be generated at each step rather than hoping one appears.
The first insight most teams miss: prevent the truncation class entirely by checking finish_reason. If the response ended because of a length limit rather than a natural stop, the JSON was never going to be complete and repair is wasted effort. Detect that and re-request with a smaller scope or a higher output budget.



