Back to blog
Article

Structured outputs so you stop parsing prose with regex

Structured outputs so you stop parsing prose with regex
S

StriveBit

3 min readAI Integration

The regex that worked on Tuesday and broke on Wednesday

A support ticket comes in. The model reads it and returns: `Priority: high, Category: billing, Confidence: 0.82`. You write a regex to pull out `high`, `billing`, and `0.82`. It works on the first fifty tickets. Then the model returns `Priority: High` with a capital H. Then `Priority: high (see notes)`. Then `Priority — high`. Then it decides to say `Urgency` instead of `Priority`.

This is not the model being unreliable. This is you asking for prose and then trying to parse it as data. The fix is to ask for data directly.

OpenAI, Anthropic, and Google all support structured output modes. You provide a JSON schema. The model is constrained — at the decoding level, in OpenAI's case — to produce output that validates against it. No regex, no best-effort extraction, no fallback for when the model decides to say "N/A" instead of a number.

Here is a schema we use for ticket triage:

from pydantic import BaseModel, Field

class TriageResult(BaseModel):
    priority: str = Field(description="one of: low, medium, high, urgent")
    category: str = Field(description="one of: billing, bug, feature, account, other")
    confidence: float = Field(ge=0, le=1)
    summary: str = Field(max_length=200)
    needs_human: bool

The call is straightforward:

completion = client.beta.chat.completions.parse(
    model="gpt-4o-mini",
    response_format=TriageResult,
    messages=[
        {"role": "system", "content": "Triage the support ticket."},
        {"role": "user", "content": ticket_text}
    ]
)

result = completion.choices[0].message.parsed

`result` is a `TriageResult` instance. If the model cannot fill it — say, the ticket is empty — you get an error you can handle, not a string you have to guess at.

The tradeoff is cost. Structured output mode adds a grammar to the decoding loop. OpenAI charges the same per token, but constrained generation can produce more tokens of overhead. In practice we see 10-15% more input tokens from the schema itself in context. For high-volume, low-value calls this matters. For triage running once per ticket on a few hundred tickets a day, it does not.

The other tradeoff is explainability. Structured output does not make the model's reasoning better, but it makes the output auditable. You get `priority: high` and `confidence: 0.82` as typed fields. You can log them, graph them, and when a ticket is misclassified, you can look at the exact values the model returned instead of wondering what the regex missed.

One thing structured output will not do: validate that the values are correct. The model will return `priority: high` with `confidence: 0.82` and be wrong. The schema guarantees shape, not truth. You still need a human in the loop for anything that triggers an action — a refund, an escalation, a customer email.

We use structured outputs for extraction tasks where the set of possible values is known and finite: classification, entity extraction, scoring. We do not use it for open-ended generation — summaries, drafts, replies — where constraining the output to a schema would either limit the model unnecessarily or require a schema so loose it provides no value.

If you are currently parsing model output with regex, the schema is worth adopting. The regex will keep breaking, and each fix is a patch against a format you do not control. The schema puts the format in your hands.

Back to all articles

Ready to build something great?

We help ambitious teams build software that lasts. If you're interested in working with us or want to discuss your project, let's connect.

Get in touch