Structured Output Repair Loops: Recover Invalid LLM JSON Without Hiding Failures
Schema-constrained generation still fails at integrations and edge cases. Build bounded repair loops that preserve evidence, validate semantics, and never duplicate side effects.
Valid JSON is not the same as a valid decision. A model can return syntactically perfect output that violates a business rule, cites a nonexistent record, or requests an action the user never authorized. A production repair loop must therefore validate layers, limit retries, and preserve the original failure.
The goal is recovery with accountability—not endlessly asking the model to “fix the JSON” until something parses.
Define one canonical schema
Keep the schema in source control and generate types or validators from it. Do not maintain a TypeScript interface, prompt example, and JSON Schema by hand; they will drift.
import { z } from "zod";
const Plan = z.object({
intent: z.enum(["read", "draft", "execute"]),
targetIds: z.array(z.string()).max(50),
rationale: z.string().max(1000),
requiresApproval: z.boolean(),
}).strict();
Strict schemas reject unknown keys that could smuggle unsupported instructions into a downstream executor. Use explicit limits for strings and arrays so a formally valid object cannot exhaust logs or databases.
Validate in four layers
Run validation in this order:
- Transport: did the provider request succeed, and was the response complete?
- Syntax: can the payload be decoded as the expected format?
- Schema: are types, required fields, enums, and bounds correct?
- Semantics and policy: do IDs exist, totals reconcile, and permissions allow the action?
Do not send every failure back to the model. A timeout or provider error needs an infrastructure retry. A missing database record needs fresh data. Only a representational defect belongs in an output-repair prompt.
Preserve the first response
Store the original response or a protected reference before repair. Attach a content hash, model ID, schema version, request ID, and validation errors.
type RepairAttempt = {
attempt: number;
originalHash: string;
schemaVersion: string;
errorCodes: string[];
repairedHash?: string;
outcome: "valid" | "invalid" | "blocked";
};
Redact secrets and personal data before observability storage. The point is to reconstruct what happened, not to create a second sensitive-data lake.
Ask for the smallest possible repair
The repair prompt should contain the invalid object, the relevant schema fragment, and machine-readable errors. Tell the model to preserve values that are already valid and return only the corrected object.
const repairInput = {
invalid: candidate,
errors: validationErrors.map(e => ({
path: e.path,
code: e.code,
expected: e.expected,
})),
constraints: [
"Do not add target IDs",
"Do not change intent",
"Return one JSON object only"
]
};
Avoid pasting the entire application prompt when only one enum is wrong. Excess context increases cost and gives the repair model more opportunity to reinterpret the task.
Bound the loop
Use at most one or two model repair attempts. If the same path fails twice, stop. Deterministic fixes can happen before a model retry—removing a Markdown code fence, normalizing a known date representation, or parsing a numeric string when the contract explicitly permits it.
Never invent missing business data. If customerId is absent, the system must fetch or request it. A repair model cannot infer an authoritative identifier.
for (let attempt = 0; attempt <= 2; attempt++) {
const parsed = Plan.safeParse(candidate);
if (parsed.success) return semanticGate(parsed.data);
if (attempt === 2) throw new OutputContractError(parsed.error);
candidate = await repair(candidate, compactErrors(parsed.error));
}
Separate planning from execution
Repair must occur before side effects. Never execute the valid fields of a partially invalid object and then ask the model to repair the rest. That creates ambiguous partial state.
After validation, map the plan into a separate executor command. Re-check authorization at the executor boundary. Use idempotency keys so a network retry cannot repeat a payment, email, or deployment.
The executor should ignore rationale. Natural-language explanation can help an operator, but it must not carry authority.
Validate semantics with code
Schema validation cannot prove that percentages total 100, dates are in an allowed period, or a selected tool is authorized for the current tenant. Encode these checks as deterministic functions.
function semanticGate(plan: Plan): Plan {
if (plan.intent === "execute" && !plan.requiresApproval) {
throw new PolicyError("execute_requires_approval");
}
for (const id of plan.targetIds) {
if (!inventory.has(id)) throw new DomainError("unknown_target", id);
}
return plan;
}
If a semantic failure depends on stale evidence, refresh that evidence and rerun the planning step. Do not disguise it as formatting repair.
Measure what the loop hides
Track first-pass validity, repair success, repeated-error rate, schema version, model version, and semantic rejection. A 99.9% final parse rate can hide a model that fails first pass 20% of the time and burns latency on repairs.
Set service-level objectives on first-pass conformance and total repair latency. Sample repaired pairs for regressions. When one field fails repeatedly, improve the schema description, simplify the contract, or split the task.
Test hostile and ambiguous outputs
Include fixtures with extra keys, huge arrays, Unicode confusables, code fences, truncated JSON, duplicate keys, invalid URLs, unauthorized IDs, and instructions embedded inside string fields. Downstream renderers must escape output, parameterize database queries, and treat model-generated URLs as untrusted.
Structured generation reduces a class of failures. It does not make output safe to execute.
Production checklist
- One versioned canonical schema.
- Strict decoding and size limits.
- Separate syntax, schema, semantic, and policy gates.
- Original-response evidence retained safely.
- One or two bounded repair attempts.
- No side effects before all gates pass.
- Idempotent execution after approval.
- First-pass and repaired validity measured separately.
A good repair loop is deliberately boring. It corrects representation, refuses to manufacture facts, and stops when the contract cannot be satisfied.
Sources
> Want more like this?
Get the best AI insights delivered weekly.
By subscribing, you agree to our Privacy Policy. You can unsubscribe at any time.
> Related Articles
Context Window Truncation Safeguards: Stop AI Agents From Forgetting Critical Instructions
Long-running agents eventually exceed a model's context window. Use explicit budgets, pinned invariants, summaries, and regression tests to prevent silent instruction loss.
MCP OAuth Security for AI Agents: Scope Tokens, Bind Resources, and Block Confused Deputies
Connecting agents to remote MCP servers expands the authorization surface. Use resource-bound tokens, PKCE, audience checks, and per-tool policy enforcement.
AI Agent State Checkpointing: Resume Long Tasks Without Repeating Side Effects
Long-running agents will crash, time out, and lose context. Durable checkpoints let them resume safely without sending the same email or charge twice.
Tags
> Stay in the loop
Weekly AI tools & insights.