Skip to content

Structured LLM Outputs: JSON Schema, Validation, Retries, and Repair

CoreConceptJuly 28, 20265 min read

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.

Structured LLM output contract: schema, prompt meaning, validation, version, and consumers
Structured LLM output contract: schema, prompt meaning, validation, version, and consumers

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.

Structured output reliability comes from contract layers, not a single instruction to return JSON.
LayerOwnsFailure If Missing
SchemaShape, enums, required fieldsDownstream code guesses
PromptTask meaning and evidence rulesValid JSON can be semantically wrong
ValidatorAccept, retry, repair, or failBad output enters the system
VersionCompatibility and rolloutOld consumers break on new fields
Structured output layers: schema, prompt, validator, and consumer
Structured output layers: schema, prompt, validator, and consumer

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.

Ticket classifier output flow: model response, parse, validate, semantic checks, publish
Ticket classifier output flow: model response, parse, validate, semantic checks, publish

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, and policy_denied terminal 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.
Parsing is not validation
1const raw = await llm.complete(prompt);2const parsed = JSON.parse(raw);3 4// This can still publish category: "billing-ish" or priority: 11.5await queue.publish("ticket.classified", parsed);
Validate before publishing
1type ClassificationResult =2  | { ok: true; version: "ticket-classifier-v2"; value: TicketClassification }3  | { ok: false; code: "invalid_json" | "schema_failed" | "low_confidence"; detail: string };4 5const raw = await llm.complete(prompt);6const result = validateClassification(raw, "ticket-classifier-v2");7 8if (!result.ok) return result;9await queue.publish("ticket.classified", result.value);

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.

Repair detail: malformed output gets one bounded correction attempt before typed failure
Repair detail: malformed output gets one bounded correction attempt before typed failure

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 schemaVersion or 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.

Failure detail: invalid priority crosses the queue because producer skipped enum validation
Failure detail: invalid priority crosses the queue because producer skipped enum validation

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.

Share:

Related Articles

A bigger foundation model is often the easiest way to get strong general behavior. A small language model can be cheaper

Read

Transformers are often described as if they are a mysterious reasoning machine. At the mechanical level, they are a repe

Read

Streaming makes an LLM app feel alive, but it also turns one clean request-response call into a lifecycle. Tokens arrive

Read

Explore this topic

Keep learning

Follow a structured path or browse all courses to go deeper.