A model returns a string.
Your component needs an object.
Everything between those two sentences is where AI features break in production, and one Zod schema can own all of it: the constraint sent to the model, the validation of what comes back, the error text that drives a repair, and the TypeScript type the component consumes.
One schema, four jobs That declaration now does four things.
It is the runtime validator.
It is the compile-time type, via , so the type and the check cannot drift apart — which is the whole argument for Zod over a hand-written interface plus a hand-written guard.
It produces the JSON Schema you send as the model’s output constraint.
And when validation fails it produces a structured error precise enough to hand back as a repair instruction.
Zod 4 ships a built-in .
Zod 3 does not, and the usual answer there is the separate package.
Check which major version is in your : several surfaces moved between the two, including where string format validators live.
This is the one version-sensitive line on the page.
The field is provider-specific and its shape differs across providers and versions — the second thing on this page to check against current documentation rather than copy.
What does not differ is the principle: where a provider supports constrained decoding against a schema, use it, because it makes malformed JSON impossible rather than unlikely.
The rest of this page is for the models where it is merely unlikely.
Parsing at the boundary Two failures live here and they need different handling: the string is not JSON, or it is JSON that does not match.
Conflating them means the repair prompt tells the model the wrong thing. rather than , deliberately: a thrown in a route handler becomes a 500 and a stack trace, when what you actually have is a recoverable situation with a known next step.
And flattening into lines is not cosmetic — that text is what the repair turn sends back, so it is worth being readable.
Repair: feeding the error back A model that produced nearly-correct JSON will usually produce correct JSON when told precisely what was wrong.
The repair turn is cheap: it re-sends only the broken output and the error, not the original document, so its input is a few hundred tokens rather than a few thousand.
One repair attempt, not a loop.
Two failures against the same explicit error message almost never become a success on the third: the model is not confused about the format, it disagrees with the schema or the document does not contain the information.
Looping there turns a fast failure into a slow expensive one, which is the general shape of what retries cost.
The escalation ladder, with a cost ceiling The ceiling is the point of the function.
Without it, a document the model genuinely cannot extract from — a scan pasted as gibberish, a field that does not exist in the source — becomes an unbounded retry loop against a paid API, and the first symptom is the bill.
Three attempts with a hard stop bounds the worst case at roughly three times the best case, which is a number you can budget for.
Return a null result rather than throwing.
The caller has a real decision to make: queue for human review, show a partial result, or ask the user to rephrase.
An exception forces all three into one .
Schemas a model can actually satisfy Half of all schema failures are the schema’s fault.
The constraints models reliably meet and the ones they reliably do not split cleanly.
Rule Description Flat beats nested Three levels of nesting produce more structural errors than three top-level objects extracted separately.
Depth costs accuracy for no gain in expressiveness.
Enums beat free strings z.enum([...]) gives the model a closed set.
A free z.string() for a category yields fourteen spellings of the same thing across a thousand documents.
Nullable beats optional An absent key is ambiguous between not-applicable and not-found.
An explicit null is a decision the model made, and you can act on it.
Describe every fie