AI APIs & Structured Outputs· Lesson 3 of 7
- 1The First Call — Reaching the Model From Code7 min
- 2Streaming — The Reply Arrives Word by Word7 min
- 3Structured Outputs — JSON You Can Rely On8 min
- 4Errors & Retries — When the Call Fails9 min
- 5Rate Limits — Your Ceiling per Minute6 min
- 6API Keys & Secrets — Guarding Your Key6 min
- 7Timeouts & Idempotency — Requests That Don't Double-Fire8 min
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.
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.
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.
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
}🧪 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.
(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.
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.
Chat and creative writing — forcing JSON there hurts text quality and burns extra tokens for no reason.