Hands-on · step by step
Tutorial: schema-validated extraction with a retry loop
by @leila — Applied research → production
Four steps to structured outputs from LLMs with Zod and JSON Schema — checkpoints, tuning knobs and the failure to guard first.
Before you start — about 45 minutes · Zod · JSON Schema · a JSON-mode model
Ship this in an afternoon. Zod does the work — the discipline is in the limits you set around it.
Step 1 — Decide what "working" means
Before touching code, write down three numbers: worst acceptable latency, cost per request, and what should happen when the upstream is unavailable. Everything after this is implementation.
Step 2 — Wire Zod
The whole flow is one path. No special branches for "large" inputs — express those as different limits, not different code.
const Invoice = z.object({
vendor: z.string(),
total: z.number().nonnegative(),
dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
lines: z.array(z.object({ sku: z.string(), qty: z.number().int().positive() })),
});
// validate first, persist second — never the other way round
const parsed = Invoice.safeParse(await extract(text));
if (!parsed.success) return retryWithFeedback(parsed.error);
Why this works
The limit is enforced before the expensive call, not after it. Reserving budget up front means a burst degrades the caller that caused it instead of everyone downstream.
Step 3 — Guard the failure path
asking for JSON in the prompt is not the same as constrained decoding
Write that guard before you write the feature. It costs an hour and saves a postmortem.
Step 4 — Verify it
npx tsx scripts/eval-extraction.ts --n 200 # prints violation rate
- Lower the limit and confirm the command fails loudly
- Kill the upstream and confirm you get a typed error, not a timeout
- Check the metric you emit per outcome: ok, throttled, upstream_error
Tuning knobs
| Knob | Default | What we run | Why |
|---|---|---|---|
| concurrency | 8 | 24 | Most of the wait is network, not compute |
| retry budget | infinite | 2, with jitter | Unbounded retries turn an outage into an avalanche |
| timeout | 30s | 8s, inherited from the caller | Nobody waits 30s for a chat reply |
Copy it, change the limits, put the verification in CI, move on.
Comments (2)
Log in to join the discussion.