What You’ll Learn

  • Why freeform model output becomes expensive in production
  • When structured output is worth the extra setup
  • How I combine zod and the Vercel AI SDK for predictable results
  • A simple fallback strategy when model output still goes sideways
  • Which classes of AI tasks benefit most from structure

The fastest way to make an AI feature feel impressive is to let the model answer in freeform text.

The fastest way to make the same feature brittle in production is also to let the model answer in freeform text.

That tradeoff shows up everywhere.

A demo works because a human is watching, interpreting the answer, and mentally fixing the messy parts. A production workflow fails because code has to consume the result exactly as it arrives.

That is why I force structured output whenever the model is feeding something downstream.

If the response is going to affect routing, persistence, automation, filtering, or UI state, I want a schema, not vibes.

Freeform Text Is Great for Reading, Bad for Control Flow

If the goal is to help a user read something, freeform text is fine.

Examples:

  • summarizing an article
  • drafting a reply
  • explaining an error
  • rewriting a paragraph

But once the model output becomes input for code, freeform text starts breaking down.

Examples:

  • classifying a support ticket
  • extracting invoice fields
  • deciding whether to escalate an issue
  • creating structured UI cards
  • turning a request into workflow actions

In those cases, I want the model to return a defined shape, even if the shape is small.

The Pattern I Reach For First

The minimal version is straightforward: define a schema, generate against it, and only continue if the result is usable.

With the Vercel AI SDK and zod, it looks like this:

import { generateText, Output } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

const triageSchema = z.object({
  category: z.enum(['bug', 'billing', 'feature']),
  priority: z.enum(['low', 'medium', 'high']),
  summary: z.string().min(1),
});

const result = await generateText({
  model: openai('gpt-4o'),
  system: 'Classify incoming support tickets for an internal triage queue.',
  prompt: 'Customer says the invoice doubled after changing plans.',
  output: Output.object({ schema: triageSchema }),
});

console.log(result.output);

What I like about this pattern:

  • the output contract is explicit
  • the result is directly usable in code
  • validation happens close to generation
  • changing the schema changes the system intentionally, not accidentally

This is much better than asking the model to “return JSON” and then hoping it behaves.

Structure Also Improves Prompt Quality

One side effect people underestimate: schemas improve prompts.

Once I know the output shape, I naturally write better instructions because I am forced to clarify what the system actually needs.

That often exposes fuzzy thinking early.

For example, if I cannot decide whether priority should be low | medium | high or a 1-5 number, the problem is not the model. The problem is that my workflow definition is still vague.

Structured output turns that vagueness into something I have to resolve.

That is a feature, not a burden.

Add a Small Recovery Path

Even with structure, you still want graceful failure behavior.

I usually keep the fallback simple:

type TriageResult =
  | { ok: true; value: z.infer<typeof triageSchema> }
  | { ok: false; reason: string };

export async function classifyTicket(prompt: string): Promise<TriageResult> {
  try {
    const result = await generateText({
      model: openai('gpt-4o'),
      system: 'Classify incoming support tickets for an internal triage queue.',
      prompt,
      output: Output.object({ schema: triageSchema }),
    });

    return { ok: true, value: result.output };
  } catch (error) {
    return {
      ok: false,
      reason: error instanceof Error ? error.message : 'Unknown generation failure',
    };
  }
}

That gives the rest of the app a clear contract. It also makes logging and retries much easier.

I do not want invalid AI output to leak into business logic as half-parsed junk.

The Best Use Cases for Structured Output

This pattern pays off most when the AI result is not the end product, but a step in a system.

My favorite categories are:

Classification

Examples:

  • ticket routing
  • lead scoring buckets
  • content moderation decisions
  • issue tagging

Extraction

Examples:

  • invoice fields
  • customer attributes
  • deployment metadata
  • action items from meeting notes

UI generation

Examples:

  • notification cards
  • tables
  • dashboard widgets
  • structured summaries for the frontend

Workflow planning

Examples:

  • create a checklist
  • produce a sequence of actions
  • return a constrained next-step plan

These all benefit from a shape the rest of the app can trust.

When I Do Not Force Structure

I am not dogmatic about it.

If the output is meant to be read directly by a person and not fed into automation, freeform text is often the better choice.

For example:

  • blog drafts
  • rewrite suggestions
  • longform explanations
  • creative ideation

The point is not “everything must be structured.”

The point is “anything important to code should probably be structured.”

Final Thought

Structured output is not just a validation trick. It is a design decision.

It forces clarity, improves reliability, and reduces the amount of fragile interpretation code you have to write later. In real AI workflows, that usually matters more than squeezing out a slightly more natural-sounding answer.

If the model is participating in a system, give it a shape to hit.

If you need help building AI workflows, structured generation pipelines, or internal tools around LLMs, take a look at my portfolio: voidcraft-site.vercel.app.