Blog / AI agents
Structured Is Not Constrained
Structured output guarantees your code can read the answer, never how many answers were possible.
Kershey Cariño · · 4 min read
In short
Structured output guarantees an answer your code can parse, not an answer your code can trust. A field typed string has an infinite set of legal values, so a schema-valid response can still route to the wrong branch. Bounding the answer space to a known set is a design decision no API makes for you.
The schema said intent was a string. The model returned "pricing_inquiry". The router was looking for "pricing". Nothing threw, nothing logged, and the lead quietly went to the default branch.
Every layer did its job. The JSON parsed, the field existed, the type matched. The only thing nobody checked was whether the value was one of the ones you'd planned for.
they answer different questions
Structured and constrained sound like the same promise. They're two different guarantees, at two different layers.
STRUCTURED "can my code read this?" a parsing guarantee CONSTRAINED "how many things could it have said?" a semantic guarantee
Structure is the envelope. Constraint is what's allowed inside it. You can have either one without the other, and most pipelines ship with only the first.
A JSON Schema constrains the container, not the content.
count the answers
There's a one-question test for any field: how many distinct values could this legally hold? Answer it and you know immediately whether the field is testable or just readable.
"is_hot" true boolean -> 2 "intent" "pricing" enum of five -> 5 "urgency" 3 integer 1-5 -> 5 "rep_id" "rep_7" id from a list -> finite, and checkable "date" "2026-08-14" ISO date -> large, but validatable "summary" "..." string -> infinite "tags" ["a", "b"] free strings -> infinite
Every one of those is valid against a reasonable schema. The last two are unbounded by nature, and a field with an infinite answer space can't be tested, only read.

None of this says the model shouldn't make the call. Often it should. It just shouldn't get to invent the options while it does.
the failure nobody gets told about
An unbounded value usually doesn't blow up. It routes. Your switch takes a value it has no branch for, falls through to the default, and a decision gets made by nobody.
| the combination | what you get | how you find out |
|---|---|---|
| unstructured, unconstrained | prose you have to dig a value out of | immediately, the parse throws |
| structured, unconstrained | valid JSON holding an unplanned value | eventually, it routes wrong and says nothing |
| structured, constrained | valid JSON holding a known value | at the boundary, the moment it's wrong |
They're separate axes, not a spectrum. Ask for one word, HOT or COLD, and the answer space is already tiny, but the model replies "I'd say HOT" and the envelope breaks anyway. You want both, and neither one gives you the other.
Malformed JSON is a failure you get told about. A well-formed value your code has no branch for is a failure that files itself and waits.
the level no API sells you
Three guarantees get collapsed into one word. Two of them are switches you turn on. The third isn't on the menu.
- it is parseable JSON. that's JSON mode.
- the fields exist, with the right types. that's schema-constrained decoding.
- the value is one of N known things. that's yours.
A provider will happily enforce an enum. Deciding the field should be an enum, and which five values belong in it, is the part that stays on your desk. Which is why "we use structured outputs" answers a different question than "is your output constrained."
- Doesn't Structured Outputs already guarantee this?
- It guarantees the shape: the JSON parses and the fields carry the right types. It has no opinion about whether the value means anything to the code reading it.
- What about fields that genuinely can't be a fixed set?
- Some are prose by nature, like a summary. Keep those out of anything that routes, decides, or writes to a record, and treat them as text for a person to read.
- How do I catch a wrong value if it's schema-valid?
- Check it against the known set at the boundary and reject what isn't in it. Rejecting is loud. Quietly coercing it to a default is how it stays hidden.
Structure is what lets your code read the answer. Constraint is what lets you trust it. Turn on the first, call it the second, and the pipeline runs green for months while filing things under a heading nobody chose.
Seen in a real build
The closed set isn't an AI idea. Here's a deploy checklist where every changed file lands in one of six tiers, and the tier is the action you're asked to take. See how QAWebLedger · Post-Deploy QA Checklist is built.
Got something to build?
Got valid JSON that keeps being wrong about intent? Fence It In! — contact Kershey, or See the Case Files.