Stop Parsing LLM Text: Structured Output as an Architectural Decision

February 18, 2026

Stop Parsing LLM Text: Structured Output as an Architectural Decision

The first version of most AI features asks a model for something and gets a paragraph back. Then you write a parser. Then the model phrases things slightly differently one day and the parser silently returns the wrong thing.

The fix isn't a better parser. It's not asking for prose in the first place.

The failure mode

Here's the shape of the problem. You ask for a content plan and get back:

Here's a plan for the week: Monday - carousel about the new product line Wednesday - a single image post, maybe testimonial-focused Friday - short video

Now you write something that splits on newlines, looks for a day name, guesses at the post type from keywords. It works on this output. It works on the next fifty.

Then the model returns "Mon." instead of "Monday", or adds a preamble sentence, or helpfully includes a fourth item you didn't ask for. Your parser doesn't throw — it just quietly produces a plan missing a day, and that flows into your database.

The dangerous part isn't that it breaks. It's that it breaks without telling you.

Schema-first instead

Define the shape you need, hand it to the model, and validate on the way out:

const PlannedItem = z.object({ day: z.enum(["mon", "tue", "wed", "thu", "fri", "sat", "sun"]), format: z.enum(["carousel", "single_image", "video", "text"]), topic: z.string().min(1), }) const ContentPlan = z.object({ items: z.array(PlannedItem).min(1).max(7), })

Now the model's output either matches that shape or it doesn't. There's no interpretive layer in between, and no way for a malformed response to pass as a valid one.

The enum is doing real work here. format can't drift into "Carousel" or "image carousel" — it's one of four values, and anything else fails validation immediately rather than three layers downstream.

Where the errors go

This is the part that matters architecturally. With a parser, a bad response becomes bad data. With a schema, it becomes an exception at the boundary — somewhere you can retry, fall back, or surface to the user.

That's the actual win. Not that the model behaves better, but that when it misbehaves you find out at the call site instead of in a user's dashboard next week.

Keep the schema honest

A few things I've settled on:

Constrain aggressively. Every z.string() that could be an enum should be one. Every array that has a real maximum should declare it. Loose schemas validate successfully and still give you garbage.

Don't nest deeply. Models handle flat structures more reliably than deeply nested ones. If you find yourself four levels in, consider two separate calls.

Version the schema alongside the prompt. They're one unit. Changing the schema without revisiting the prompt is how you get a model confidently filling a field it doesn't understand.

Make optional fields genuinely optional. If you mark something optional because the model sometimes omits it, you've hidden a prompt problem inside your type definition. Either it's required and the prompt should say so, or it's actually optional.

The part people skip

Structured output doesn't make the model correct. A schema guarantees the shape, not the quality — a plan can be perfectly valid and still be a bad plan.

What it buys you is the ability to reason about everything downstream. Every function past that boundary can trust its input, which means the interesting bugs are about content rather than parsing. That's a much better class of problem to spend your time on.

GitHub
LinkedIn
X