Tip: Use Structured Outputs to Eliminate JSON Parsing Headaches
If your LLM application ever crashes because a model wrapped its answer in a markdown code fence or added a friendly “Sure, here’s the JSON!” before the actual data, you already understand the problem. Structured outputs solve it at the root.
Why free-form text parsing fails
One of the fastest improvements you can make to any LLM-powered application is switching from free-form text outputs to structured, schema-validated responses. The difference in reliability is dramatic, and it shows up immediately the moment you move from a demo to something real users depend on.
When a model returns unstructured text and you parse it with regex or manual string manipulation, you are building a system that breaks on any variation in phrasing or formatting. The trouble is that models are probabilistic. The same prompt can produce slightly different wrapping, spacing, key casing, or ordering on different runs. A parser that worked perfectly in testing will fail in production the first time the model decides to explain itself before answering, or returns null where you expected a string, or emits a trailing comma that breaks your JSON loader.
Here are the failure modes you will hit again and again with hand-rolled parsing:
- Markdown fences. The model wraps valid JSON in triple backticks, sometimes with a language tag, and your
json.loadscall chokes on the fence. - Conversational preamble. “Here is the information you requested:” prepended to the payload.
- Inconsistent keys.
full_nameone run,namethe next,fullNamethe run after that. - Type drift. A field that should be a number arrives as the string “42”, or a boolean arrives as “yes”.
- Partial truncation. The response gets cut off and you receive half an object.
You can try to defend against each of these with more regex and more string cleaning, but that path never ends. Every patch handles the case you just saw and misses the next one. Models are not consistent enough for manual parsing to work reliably at scale, and the cost of that inconsistency is paid by whatever sits downstream of your parser.
The fix: define a schema and constrain the output
The solution is to stop hoping the model formats things correctly and instead require it to. You define a schema that describes exactly what you need — field names, types, which fields are required, and any constraints — and you use that schema to constrain and validate the model’s response.
In Python the common approach is a Pydantic model paired with a library like instructor, which patches your LLM client so that instead of returning a raw string it returns a validated object. The library injects the schema into the request, parses the response, validates it against your model, and retries automatically if validation fails. You get structured data back or a clear, typed error — not a garbled string you have to interpret.
A minimal example looks like this. You describe the shape of the data once:
- Define a class with typed fields, for example a
CustomerInquirywithcategory,urgency, andsummary. - Constrain values where it matters — make
categoryan enum of allowed labels andurgencyan integer between 1 and 5. - Call the model with that class as the expected response type.
- Receive a fully populated, validated object you can use directly.
If the model returns something that does not fit — a category outside your enum, a missing field, an urgency of 9 — the validation step catches it. The library then sends the validation error back to the model and asks it to correct the output. Most malformed responses get fixed on the first retry because the model now has a precise description of what went wrong.
What you gain beyond clean parsing
Schema enforcement does more than prevent crashes. The schema becomes documentation that both you and the model read. When you add a field description like “the customer’s account tier, one of free, pro, or enterprise,” that description travels into the prompt and steers the model toward the right answer. Your data contract and your prompt instructions stay in one place instead of drifting apart.
You also get editor support. Because the output is a typed object, your IDE knows the field names and types, and you catch mistakes while writing code rather than at runtime. Downstream functions can declare that they accept a CustomerInquiry, and the type system enforces that the right shape flows through your pipeline.
Where to apply it
Apply this pattern to every agent action that produces data consumed by another system. Anywhere the output of a model becomes the input to code, structured outputs belong. The clearest candidates:
- Tool call arguments. When an agent decides to call a function, the arguments must match the function signature exactly. A schema guarantees the model produces arguments your tool can accept.
- Extracted entities. Pulling names, dates, amounts, or identifiers out of documents or messages. The schema defines the fields and types, and validation rejects anything malformed.
- Classification results. Routing a ticket into a category, scoring sentiment, or tagging content. An enum field forces the model to choose from your defined labels instead of inventing a new one.
- Routing decisions. When one agent decides which downstream agent or workflow handles a task, the decision needs to be a clean, predictable value the router can switch on.
- Multi-step plans. When a model outputs a list of steps for an agent to execute, each step can be a validated object with an action and parameters.
The common thread is that a human is not reading these outputs — a machine is. Free-form prose is fine when a person is the consumer and can tolerate variation. The moment code consumes the output, variation becomes a liability, and a schema removes it.
Practical tips for getting schemas right
Defining a schema is easy; defining a good schema takes a little judgment. A few habits make the difference between a schema that helps and one that fights you:
- Constrain values, not just types. Use enums for categories, numeric bounds for scores, and required versus optional flags deliberately. The tighter the schema, the less room the model has to drift.
- Write field descriptions as instructions. Treat each description as a small piece of the prompt. “ISO 8601 date, or null if no date is mentioned” is far more useful than “the date.”
- Model uncertainty explicitly. If the model might not find a value, make the field optional or add an explicit
not_foundoption rather than forcing it to guess. Forcing a required field invites hallucination. - Keep schemas flat where you can. Deeply nested objects are harder for models to populate reliably. Flatten when the data allows it, and reserve nesting for genuinely hierarchical data.
- Set a sensible retry limit. Automatic retries are powerful, but cap them — usually two or three attempts. If the model cannot satisfy the schema after a few tries, you want a clean error, not an infinite loop burning tokens.
- Validate semantics, not just structure. A response can be structurally valid and still wrong — a date in the future when it should be in the past, for instance. Add custom validators for business rules that the type system cannot express.
A note on native structured output modes
Many model providers now offer a built-in structured output or JSON mode that accepts a schema directly. When available, these are worth using because the constraint happens during generation rather than after, which reduces malformed responses in the first place. Libraries like instructor often use these native modes under the hood when they can and fall back to retry-based validation when they cannot. You do not have to choose between them — pick the library, and let it use the best available mechanism for your model.
The overhead is small and it compounds
The cost of adopting this pattern is genuinely minimal. You write the schema once, usually a dozen lines, and you gain confidence that downstream systems get well-formed inputs every time. There is no ongoing maintenance burden of patching parsers against new failure modes, because the schema handles the whole class of problems at once.
That confidence compounds as your pipeline grows more complex. In a single-step script, sloppy parsing is annoying but survivable. In a multi-agent system where one agent’s output feeds the next, an unvalidated handoff is a silent failure waiting to corrupt everything downstream. Schema enforcement at each boundary means failures surface immediately, with a clear error pointing at exactly where the contract broke, instead of propagating into mysterious behavior three steps later.
Takeaway
Stop parsing model output by hand. Define a schema for any data a machine will consume, constrain the fields as tightly as the task allows, and let a library handle validation and retries. Start with your highest-risk boundary — usually tool calls or any place one agent feeds another — convert it to a validated schema, and expand from there. You will write less defensive code, ship fewer parsing bugs, and build a pipeline you can extend without fear that the next change breaks the last one.