Structured outputs instead of parsing free text
Why you shouldn't scrape data out of model replies with regexes, how to define a schema, validate the result and what to do when validation fails.
The first version of almost every model-backed feature has a line like “reply in the format: Category: …, Priority: …”. Then the code splits the text on colons. It works for a week. Then the model replies “Sure! Here’s the result:”, adds asterisks for bold, or writes “Priority: urgent” instead of high, and the parser breaks without a sound.
The model isn’t the problem. You asked for text and got text. If your code needs data, ask for data.
Ask for JSON against a schema
Most major providers can return output that conforms to a JSON schema you supply, either through a dedicated structured-output mode or through a tool call whose arguments are defined by a schema. If that mode exists, use it instead of asking nicely in the prompt. It’s the difference between “please fill this in carefully” and a form with required fields.
The schema states exactly what you need:
{
"type": "object",
"properties": {
"category": { "type": "string", "enum": ["refund", "delivery", "other"] },
"priority": { "type": "string", "enum": ["low", "normal", "high"] },
"order_id": { "type": ["string", "null"] },
"summary": { "type": "string", "maxLength": 200 }
},
"required": ["category", "priority", "order_id", "summary"]
}
A few habits that save time:
- enums over free strings: if there’s a finite set of values, list them. Then “High,” “urgent” and “very important” never show up;
- explicit
null: if a value may be absent, allownulland say so in the field description. Otherwise the model will invent an order number just to fill the slot; - field descriptions: a short hint about what goes in a field works better than a long paragraph in the prompt;
- flat structure: the deeper the nesting, the more places to go wrong. If one level will do, use one level.
Validate anyway
Even in strict mode, check the result in your own code. First, not every provider and model guarantees schema conformance equally. Second, a schema checks shape, not meaning: order_id can be filled with a number that never appeared in the input.
Validation has two layers:
- shape: types, required fields, allowed values. Every language has libraries for this, and one schema can drive both the model request and the check;
- meaning: business rules. The order exists and belongs to this customer, the amount isn’t negative, the date isn’t from the last century.
Whatever passes becomes ordinary typed data. Whatever fails goes no further, not into the database and not to the user.
When validation fails
Don’t re-ask forever. A sensible sequence:
- one retry with the error: send the model its own output plus the specific validation error, such as “priority must be one of low, normal, high.” That usually does it;
- a fallback: if the retry fails too, the feature should behave predictably: a default value, a “needs human review” flag, or an honest error in the UI;
- a log entry: store every failure with its input and output. If one field fails regularly, the schema or its description is the problem, and you’ll only see that across accumulated cases.
Keep reasoning and data apart
Sometimes you want the model to think before it answers. That’s fine, but keep the reasoning in its own schema field or a separate step, not in prose wrapped around the JSON. Code reads the fields, a human debugging reads the reasoning, and nobody gets confused.
The upshot
Free text is for people, structure is for code. Define a schema, use the provider’s structured-output mode if it has one, validate shape and meaning, and decide up front what happens on failure. Then the model’s reply stops being a string nobody dares touch and becomes ordinary validated input.
SUBXCAT can move your product’s features from free text to validated schemas and find where parsing is quietly failing today.