T1 · Intermediate · 8 minMoving · Last reviewed: 2026-08-19Early draft

Structured Outputs — JSON You Can Rely On

In one line · Instead of free text your code can't trust, you ask the model to return JSON in a fixed shape (a schema) — and you still validate it against that schema before you rely on it.

Glossary:HallucinationJSON SchemaModelProbabilisticPrompt
Why it matters

Anywhere the model feeds a database or UI needs clean data. Without structure you'll keep writing regex and patching breakage — the biggest bug source in AI apps.

It's an essay versus a form with labeled boxes: the essay reads nicely but needs interpreting; the form has each value in its slot, ready to drop straight into place. Just keep in mind — you still check every box was filled correctly.

An analogy

Like customs at a border: instead of letting people through with their bags in a jumble, you make them fill a form with fixed fields, and any form that's incomplete or wrong is sent back before it passes.

Instead of returning 'the customer is upset about the delay', you request {sentiment:'negative', topic:'delivery'} with a strict schema — now you can filter, count, and chart it without guessing, and any reply that breaks the schema you reject instead of storing.

How it works

You define a schema (JSON Schema or Zod) and send it with the request via response_format or its equivalent, so the model is constrained to that shape while generating. But the constraint isn't a full guarantee: the model can refuse to answer, or return logically wrong values inside a correct shape. So you validate against the schema after the reply arrives and reject anything that breaks it — retrying or logging the failure, but never storing it.

import { z } from "zod";

const Ticket = z.object({
  sentiment: z.enum(["positive", "neutral", "negative"]),
  topic: z.string().min(1),
});

const data = await callModel({
  messages,
  response_format: { type: "json_schema", json_schema: ticketJsonSchema },
});

const parsed = Ticket.safeParse(JSON.parse(data.choices[0].message.content));
if (!parsed.success) {
  // reject: retry or log — never store unvalidated output
}
Practice

🧪 Try it yourself: take 5 real support messages and have the model extract {sentiment, topic} with a strict schema using an enum for sentiment. Validate every reply with Zod, count how many get rejected — then try without structured mode and compare.

Common mistakes

(1) Trusting that 'right shape = right facts': a reply that passes the schema can still hold wrong values — the schema only guarantees shape. (2) Asking for JSON in the prompt alone and calling JSON.parse directly — models often wrap the reply in ```json fences and the parse crashes; use a real structured mode. (3) Writing a loose schema where everything is a string — enums and constraints do half the work.

When to use it

Anywhere the model's reply feeds code: a database, UI, filtering, a pipeline — if anything other than a human (i.e. code) reads the reply, you need structure plus validation.

When NOT to use it

Chat and creative writing — forcing JSON there hurts text quality and burns extra tokens for no reason.

LET'S BUILD ITDone reading — now build it yourself.Turn a free reply into schema JSON