The regex era
Early in building with language models, a lot of our code looked like this: ask the model for a result, receive a friendly paragraph, then write a parser to dig the facts out of it. The parser used regular expressions. The regular expressions were cursed.
Some days the model wrote "Amount: 5,000". Other days "The total is NGN 5000.00". Once it wrote the number in words. Each variation produced a bug, and each bug produced another pattern, until the parsing code was longer than the feature it served.
Structured output ended that. We now think it is one of the most practical changes in how software uses language models, and it deserves less mystique than it gets.
What it is
Instead of asking the model to write prose and hoping, you give it a schema: the fields you want, their types, which are required, which values are allowed. The model's response is constrained to match. You receive an object, not a paragraph.
Different providers implement this differently. Some constrain generation directly so that invalid output cannot be produced. Others rely on tool calling, where the schema describes a function the model "calls". The details vary and change over time, so check what your provider guarantees and what it merely encourages. The idea is the same in each case.
A typical schema, written in TypeScript style, might look like this:
type InvoiceExtraction = {
supplier: string
currency: "NGN" | "GHS" | "GBP" | "USD" | "other"
amountMinor: number
dueDate: string | null
needsReview: boolean
reviewReason: string | null
}
Notice the choices. Money is an integer in minor units, so there is no decimal ambiguity. Currency is an enumeration, so "Naira" and "NGN" cannot both appear. A missing due date is an explicit null, not an invented one.
What it changed in practice
The model became a component. Before, the model was a collaborator whose answers needed interpretation. Now it is a function with a typed return value. Other code can depend on it, test it and replace it.
Validation moved to the edge. We still validate the object, because a well formed answer can be a wrong one. But the validator now checks meaning (does the amount make sense, is the date in the future) and not shape. The two kinds of failure are handled differently, and that clarity helps.
Failures became legible. When output is free text, a bad result is a mystery. When it is an object, you can see which field was wrong, log it, and count how often. Patterns show up that prose hid.
Prompts got shorter. A great deal of old prompt text existed to coax a format: "respond only in JSON, do not add commentary". The schema does that job better, and the prompt can go back to describing the task.
The habits we have kept
Give the model a way out. Fields like needsReview and reviewReason, or nullable values, let it say "I am not sure" without inventing something to fill the gap. A schema that forces an answer will get one.
Keep schemas small and specific. One schema per task, with fields that mean one thing each. Large, loose schemas bring the old ambiguity back with better formatting.
Describe the fields. A short description on each property is read by the model and acts as part of the instructions. It is the cheapest accuracy improvement we know.
Version your schemas. Downstream code depends on them, so changing a field is a breaking change, and should be treated as one.
What it does not fix
A valid object is not a true one. The model can still misread an invoice and return a perfectly shaped, perfectly wrong amount. Structure removes formatting errors, not reasoning errors. Anything that matters, such as money or access, still needs checks that do not trust the model, and often a person for the doubtful cases.
Constrained output can also, in some setups, nudge quality in subtle ways, particularly where the schema forces the model to commit to an answer before it has reasoned. If your task needs thinking first, give the schema a field for reasoning that comes ahead of the answer, and measure the result on your own examples instead of assuming.
Testing a structured pipeline
Once the output is an object, testing gets ordinary, which is the best thing about it. We keep a set of real inputs with the correct objects beside them, run the pipeline against the set whenever a prompt, schema or model changes, and compare field by field. A change that improves supplier names and quietly worsens due dates is now visible in a single run, where before it would have been a feeling.
Two details help. First, test the awkward inputs on purpose: the blurry scan, the invoice in two currencies, the one with no date. Second, record which cases the model flagged for review, and check that the flagged ones are the ones it really got wrong. A model that is unsure about the right things is more useful than one that is slightly more accurate and confident about everything.
We also keep the schema next to the code that consumes it, so a change to one forces a look at the other. It sounds trivial. It prevents the kind of drift where the prompt promises one shape and the database expects another.
Cost and speed
Structured output has small costs worth knowing. Schemas add tokens to each request. Strict constraints on generation may add a little latency on the first call with a new schema in some providers. For most workloads this is negligible next to the engineering time saved, but if you run at very high volume, measure it.
Where to begin
Find the place in your codebase where you parse model output with string handling. Replace that one place with a schema. Keep the old path behind a flag for a week, compare the results, and look at how much code you can delete. In our experience it is more than you expect.
