Structured LLM Outputs: JSON Schema, Validation, Retries, and Repair
LLMs are fluent text generators; production systems need contracts. The gap shows up when a classifier returns urgent-ish, omits a required field, wraps JSON in markdown, or changes shape after a prompt edit. The model may have answered well in English while still breaking the system that consumes it.
This guide is for developers building LLM features that feed APIs, queues, dashboards, or workflow automation. You will define a ticket-classifier JSON contract, validate it, retry only bounded failures, and store typed terminal states. For surrounding controls, connect this with Prompt Engineering for Developers and AI Gateway: Routing, Cost Controls, and Observability.
The Contract Is Bigger Than JSON
A structured output contract has four layers. The schema names fields and allowed values. The prompt explains the task and expected meaning. The validator decides whether the model output can enter the application. The consumer assumes the validated shape is true enough to act on. JSON is only the serialization format between those layers.
The running example is ticket-classifier. Input is a support ticket. Success is { category, priority, confidence, reasons } where category and priority are enums, confidence is a bounded number, and reasons are short evidence strings. The intentional failure is a model returning a plausible but invalid priority such as urgent-ish; the app must reject or repair it before a queue route changes.
| Layer | Owns | Failure If Missing |
|---|---|---|
| Schema | Shape, enums, required fields | Downstream code guesses |
| Prompt | Task meaning and evidence rules | Valid JSON can be semantically wrong |
| Validator | Accept, retry, repair, or fail | Bad output enters the system |
| Version | Compatibility and rollout | Old consumers break on new fields |
Quick reference
- Use enums for routing decisions; free text is poor input to queues and state machines.
- Keep model-facing field descriptions close to the runtime schema so they do not drift.
- Validate both syntax and semantics: JSON parse, schema shape, enum values, ranges, and cross-field rules.
- Store output version with every accepted result.
- Treat low confidence as a typed outcome, not a malformed result.
Remember this
Reliable structured output is schema plus meaning plus validation plus versioning; JSON alone only proves the model produced parseable text.
One Ticket Through the Output Path
The path starts before the model call. Normalize the ticket input, choose the schema version, and send the task with clear field semantics. After the model responds, parse the JSON, validate it against the schema, run cross-field checks, and only then publish the classification or update a ticket. If validation fails, decide whether the error is repairable.
Repair is useful for formatting mistakes, missing optional fields, or enum spelling that can be mapped safely. It is not a license to keep asking until the model says what you wanted. If the model gives a valid shape with weak evidence, that is a quality or confidence failure, not a syntax failure. Route it to human review or a safer default instead of hiding it under retries.
Quick reference
- Keep raw output in restricted traces; store validated output in application records.
- Use the same schema in prompt construction, validation, tests, and documentation.
- Separate
invalid_json,schema_failed,low_confidence, andpolicy_deniedterminal states. - Publish only validated values to queues, workflows, and databases.
- Add a golden set with valid output, malformed JSON, invalid enum, and low-confidence cases.
Remember this
The safe path is model output → parse → schema validation → semantic checks → typed terminal state; publishing directly after JSON.parse is still unsafe.
Retries and Repair Need a Budget
Every retry is another model call with cost, latency, and possible new failure. Use retries for bounded, observable classes: malformed JSON, missing required field, or enum value outside the schema. Give the repair prompt the validation error and the original raw output, then ask for the same schema again. Cap attempts and record that repair occurred.
Do not retry semantic disagreement forever. If the schema is valid but confidence is low, evidence is missing, or the category conflicts with a business rule, the right terminal state may be needs_review. A repair loop should make the output conform to the contract, not launder uncertainty into a confident answer.
Quick reference
- Set a small repair limit, commonly one retry for user-facing latency-sensitive paths.
- Repair syntax and schema errors; escalate semantic uncertainty.
- Log attempt count, validation error, repaired fields, model route, and final terminal state.
- Make repair prompts deterministic in shape: no new task, no new policy, only contract correction.
- Stop retries on repeated same-field failure; that usually means prompt/schema mismatch.
Remember this
Repair loops are for bounded contract violations, not for forcing the model through uncertainty until a convenient answer appears.
Version the Output Before Consumers Depend on It
Structured output becomes an API the moment another component consumes it. A dashboard, queue worker, or CRM integration may depend on the exact enum names and required fields. Adding escalationTeam, renaming priority, or changing confidence from 0..1 to 0..100 can break consumers even if the model still returns valid JSON.
Version the output contract deliberately. Add optional fields first, keep old fields during migration, and publish versioned events or records. If an old consumer sees a new version it does not understand, it should fail closed or route to review rather than guessing. Structured outputs deserve the same compatibility discipline as REST payloads or message schemas.
Quick reference
- Put
schemaVersionor an equivalent output version on every accepted result. - Avoid renaming enum values casually; downstream routing often depends on exact strings.
- Roll out new required fields behind a consumer readiness check.
- Keep contract tests for each active consumer, not only the prompt harness.
- Deprecate old versions with metrics showing who still reads them.
Remember this
A structured LLM response is an API payload once another system consumes it, so version it before route workers and dashboards depend on its exact shape.
Recover an Invalid Priority Incident
Trigger. A prompt edit asks the model to be more nuanced about urgency. Symptom. Some tickets start publishing priority: "urgent-ish", and the queue worker silently maps unknown priority to normal. Root mechanism. The app parsed JSON but did not validate the enum before publishing, and the consumer had a permissive default.
Recovery: reject unknown enum values at the producer, add a repair attempt that maps only to allowed priorities, remove the consumer's permissive default, and replay affected tickets from raw traces. Prevention: add a regression case for invalid enum values, require a typed schema_failed state, and block prompt releases unless the golden set includes malformed JSON, invalid enum, low confidence, and valid happy path.
Quick reference
- Find affected records by output version, prompt version, and raw enum value.
- Fix the producer first so new invalid events stop entering the queue.
- Fix the consumer default so unknown values fail visibly.
- Replay only after the schema and consumer behavior are corrected.
- Add an alert on schema-failure rate and repair-success rate after prompt releases.
Remember this
Invalid structured output incidents are usually contract incidents: reject bad values before publish, make consumers fail visibly, and replay with version evidence.
Key takeaway
Structured outputs turn LLM text into application data only after a real contract boundary. Define a versioned schema, teach the model the field meanings, validate before publishing, repair only bounded contract failures, and keep terminal states visible.
Practice (30 min): build a ticket-classifier-v1 schema with category, priority, confidence, and reasons. Expected success: a valid ticket produces ok: true and publishes one event. Intentional failure: make the model return priority: "urgent-ish" and verify schema_failed before publish. Recovery: add one repair attempt that chooses from allowed priorities only. Pass when happy path, malformed JSON, invalid enum, and low-confidence output all produce distinct terminal states.
Related Articles
Explore this topic