Reliable Structured Outputs: Schemas, Validation & Failure Handling
Key distinction: syntactically valid JSON is not the same as a correct business result. A schema can constrain shape and types; application validation must still enforce meaning, authority, and safety.
Structured outputs let an application request data that conforms to a declared shape rather than parsing free-form prose. Provider interfaces change, but the durable engineering pattern is consistent: define a narrow contract, request structured output through a supported schema or tool interface, validate again in your application, and handle refusal or incomplete generation explicitly.
Start from the consumer
Design the schema around what downstream code needs, not everything the model could say. Avoid an unbounded object with dozens of optional fields. Required fields, enums, length limits, and explicit nullable values make failures visible.
{
"type": "object",
"additionalProperties": false,
"properties": {
"severity": {"type": "string", "enum": ["low", "medium", "high"]},
"summary": {"type": "string", "minLength": 1, "maxLength": 240},
"requires_human_review": {"type": "boolean"}
},
"required": ["severity", "summary", "requires_human_review"]
}
What schema enforcement can guarantee
| Property | Schema can help | Application must still check |
|---|---|---|
| Syntax | Object, array, number, boolean, required keys | Provider response completed normally |
| Vocabulary | Enums and patterns | The selected value is supported by evidence |
| Bounds | Length and numeric ranges where supported | Domain-specific limits and cross-field rules |
| Authority | Nothing | Whether the caller may perform the requested action |
| Truth | Nothing by itself | Grounding, citations, database state, and business rules |
A robust response pipeline
- Declare a versioned contract. Store the schema with application code and give it an explicit version.
- Use a provider-supported structured interface. Depending on the provider, this may be a response schema, structured-output mode, or a tool definition.
- Detect non-success states. Handle refusals, safety blocks, truncation, network errors, and tool-call errors separately.
- Validate locally. Run a standard JSON Schema or typed-model validator even if the provider says the shape is constrained.
- Apply business rules. Verify identifiers, permissions, cross-field relationships, and references against authoritative systems.
- Execute only after authorization. Treat model output as a proposal, not a permission grant.
Handle failure without hiding it
Do not silently coerce malformed or incomplete output into a valid-looking object. Return a typed error such as refused, incomplete, schema_invalid, or business_rule_failed. Retry only failures that are likely transient, with a small limit and an idempotent downstream action.
Schema design rules
- Set
additionalPropertiesto false when extra keys are not useful. - Prefer enums over unconstrained status strings.
- Represent “unknown” explicitly instead of forcing a guess.
- Use stable identifiers, not display names, for database references.
- Keep actions separate from explanations: one machine contract and one optional human-readable field.
- Avoid embedding executable code or unrestricted URLs unless the application has a strong validator and sandbox.
Versioning and compatibility
Changing a field name or enum can break consumers even when the model call succeeds. Add a schema_version, keep compatibility tests, and deploy producers and consumers with an intentional migration plan. Capture validation failure rates by schema version without logging unnecessary sensitive prompt data.
Security boundary
A valid object can still contain a destructive path, an internal URL, a stolen identifier, or a command that should never execute. Perform path normalization, URL allowlisting, authorization, and target confirmation outside the model. For agents, require human approval before consequential writes even when the tool arguments pass schema validation.
Evaluation checklist
- Valid ordinary examples and empty-state examples
- Ambiguous inputs where “unknown” is correct
- Adversarial instructions attempting extra keys or forbidden actions
- Long inputs that may cause incomplete output
- Refusal and provider-error handling
- Old and new schema-version compatibility
- Business-rule violations that are syntactically valid
Worked contract: triage a support ticket
Illustrative design—not production telemetry. Suppose a help desk wants the model to propose a queue and urgency while ordinary application code decides whether the ticket may be routed. The consumer needs a small contract, not a free-form diagnosis.
{
"schema_version": "ticket_triage.v1",
"queue": "billing | access | bug | unknown",
"urgency": "normal | urgent",
"evidence": ["short excerpt from the ticket"],
"needs_human_review": true
}The JSON Schema can require the keys, enums, array type, and string lengths. It cannot prove that an excerpt appears in the original ticket, that “urgent” follows company policy, or that the requester may see the selected queue. Those are semantic checks:
- Reject an evidence string that is not a normalized substring of the submitted ticket.
- Require
needs_human_review=truewhen the queue isunknown. - Permit
urgentonly when a deterministic rule detects an approved signal such as account lockout or active data loss. - Resolve the queue to an internal identifier from a server-side allowlist; never execute a model-supplied URL.
- Record the schema version, model snapshot, validation outcome, and final human override.
Separate transport, schema, and semantic failures
| Observed state | Typed outcome | Safe response |
|---|---|---|
| Timeout, rate limit, or network interruption | transport_error | Retry with backoff only when the operation is idempotent |
| Provider refusal or safety block | refused | Do not disguise it as an empty successful result |
| Generation ended before the object completed | incomplete | Retry within a fixed budget or send to review |
| Object violates the declared contract | schema_invalid | Log the validator path; do not repair silently |
| Valid shape but unsupported conclusion | semantic_invalid | Reject or require a human decision |
| Valid proposal but caller lacks authority | forbidden | Stop before any side effect |
Build consumer-driven contract tests
Keep a provider adapter behind one application interface so a model or SDK change does not leak into every consumer. Test the adapter with fixtures for success, refusal, truncation, unknown enum values, extra properties, and valid-but-dangerous arguments. A compact test can express the real boundary:
result = classify_ticket(fixture)
assert result.kind == "ok"
assert validate_schema(result.value, "ticket_triage.v1")
assert evidence_is_grounded(result.value.evidence, fixture.text)
assert authorize(fixture.user, result.value.queue)
Run the same fixtures when the prompt, model snapshot, SDK, schema, or provider changes. For generated fuzz cases, vary missing fields, Unicode, extreme lengths, duplicate-looking identifiers, URLs, path traversal strings, and prompt-injection text.
Measure more than JSON validity
| Metric | Question answered | Suggested denominator |
|---|---|---|
| Completion rate | Did the provider return a terminal response? | All requests |
| Schema pass rate | Did completed output satisfy the declared shape? | Completed, non-refused requests |
| Semantic pass rate | Did values satisfy grounding and business rules? | Schema-valid outputs |
| Refusal detection recall | Were refusal states surfaced as refusals? | Known refusal fixtures |
| Unsafe execution rate | Did any rejected proposal reach a side effect? | Adversarial fixtures |
| Override rate | How often did reviewers change a valid proposal? | Human-reviewed outputs |
A safe schema migration sequence
- Add a new schema version rather than changing an existing contract in place.
- Teach the consumer to accept both versions and normalize them into one internal type.
- Run replay tests against representative stored inputs, with sensitive data redacted.
- Deploy the new producer to a small traffic slice and compare validation and override rates.
- Stop producing the old version only after every consumer is compatible.
- Retire the old parser on a separately reviewed release, with a rollback path retained.
This sequence prevents a “successful” model rollout from becoming a downstream outage. It also gives incident responders a precise schema version to search in traces.
Primary references
Bottom line
Use provider constraints to reduce formatting failures, then validate shape, meaning, permissions, and side effects in ordinary application code. The schema is a contract—not a trust decision.
What retries cost
Structured output failures are handled by retrying, and retries are the hidden line in most structured-output bills. Budgeting for the retry rate rather than the success rate is the difference between an estimate and a forecast. Below: one request at 12K input, 6K cached, 3K output. Of the 12,000 input tokens, 6,000 are billed at the cache-read rate and 6,000 at full input rate.
| Model | Cost per request | Monthly at 15,000 requests |
|---|---|---|
| DeepSeek V4.1 Flash | $0.0027 | $40.77 |
| GPT-5.6 Luna | $0.0049 | $73.80 |
| Gemini 3.8 Flash | $0.016 | $243 |
| Claude Haiku 4.5 | $0.022 | $324 |
Even at fifteen thousand requests the whole month is cheap, which means a retry rate of a few percent is affordable and strict schema validation is worth enforcing. The cost of retries is not the tokens; it is the latency they add to a synchronous path.
Rates verified against provider documentation on September 18, 2026. Promotional rates expire, so re-check before budgeting: LLM API cost planning · September 2026 pricing update. Run your own numbers in the cost calculator.