Last updated: Aug 25, 2026

Structured LLM Outputs: Validation, Retries, and Failure Handling

Dan Lee, JoinAI Founder · AI Tech Lead

JoinAI Founder · AI Tech Lead

Aug 25, 20263 min read
Structured LLM output passing through schema, semantic, policy, and side-effect validation gates

Structured output solves syntax, not truth. A model can return JSON that matches your schema and still select the wrong customer, misuse an enum, or request an unauthorized refund. Treat generation as the first step in a typed boundary: enforce the provider schema, validate business semantics, check policy, and only then allow downstream effects.

Retry only failures that a new generation can plausibly repair. Route refusals, exhausted attempts, and policy violations into explicit terminal states instead of hiding them behind a generic parser error.

Use four validation gates

GateQuestionExample failureCorrect response
TransportDid the request complete with the expected response type?Timeout or truncated streamRetry under request policy or return unavailable
SchemaDoes the value match required types, enums, and shape?amount is a stringOne bounded repair attempt or invalid output
SemanticsIs the value valid for current application state?Order is already refundedReject; refresh state only when safe
PolicyMay this principal perform this action?Refund exceeds approval limitDeny or require approval outside the model

OpenAI supports strict JSON Schema output for compatible models while noting that only a subset of JSON Schema is supported. Anthropic tool definitions use an input_schema JSON Schema object. Provider enforcement reduces malformed output, but the application still owns semantic and authorization checks. See the OpenAI response-format reference and Anthropic tool-use implementation guide.

Design the schema as an application contract

Prefer a small tagged result over an object full of optional fields:

JSON
{
"type": "object",
"additionalProperties": false,
"properties": {
"decision": { "enum": ["approve", "reject", "needs_review"] },
"reason_code": { "enum": ["policy_match", "missing_evidence", "limit_exceeded"] },
"evidence_ids": { "type": "array", "items": { "type": "string" } }
},
"required": ["decision", "reason_code", "evidence_ids"]
}

Do not ask the model for values the server already knows, such as user identity, tenant, current time, or permission level. Join model output with trusted server state after validation.

Version the schema. Store schema, model, prompt, and validator versions on the task trace. A compatible provider API does not guarantee identical model behavior across snapshots; OpenAI recommends pinned model versions and evals when consistency matters. See its backward-compatibility guidance.

Make failure states explicit

TypeScript
type StructuredResult<T> =
| { state: 'accepted'; value: T; attempt: number }
| { state: 'refused'; reason: string }
| { state: 'invalid'; errors: string[]; attempts: number }
| { state: 'unavailable'; retryAfterMs?: number };

Keep refusal separate from schema failure. A refusal may be a valid response to the task, not broken JSON. Keep transport failure separate from invalid content so the caller applies the right retry policy.

Retry with a bounded repair envelope

One repair attempt is often enough to distinguish a transient formatting failure from a broken contract. Send validation errors, the original schema version, and the original input; do not silently broaden the schema.

Do not retry a policy denial, missing source data, an invalid user-supplied resource, a side effect with uncertain outcome, or repeated semantic failure with unchanged state. If a model call precedes a consequential tool, the idempotency and retry boundary must own uncertain execution.

Test the contract, not just happy JSON

Text
valid object → accepted
extra property → invalid
unknown enum → invalid
valid order ID from another tenant → policy denied
already-refunded order → semantic invalid
provider refusal → refused
timeout before response → unavailable
timeout after side effect → uncertain, never regenerate action

Track acceptance, refusal, schema failure, semantic failure, policy denial, repair success, and latency per attempt. Put them on one task trace using the production LLM observability schema.

The release gate is not “99% valid JSON.” It is task success after every gate, with dangerous false accepts reported separately. Preserve failures as regression cases with the LLM evaluation dataset workflow.

Use structured outputs when downstream code needs a stable contract. Use ordinary text when the user is the consumer and rigid structure does not improve the task.

Sources and further reading

Build better AI systems

One practical engineering lesson in your inbox each week.

JoinAI Premium

Go from reading to shipping

Get guided learning, hands-on AI engineering projects, and premium practice.

Explore Premium
Dan Lee, JoinAI Founder · AI Tech Lead

About the author

JoinAI Founder · AI Tech Lead

Dan Lee is the founder of JoinAI and an AI tech lead with more than 10 years of industry experience across data engineering, machine learning, and applied AI. He previously worked as an engineer at Google.