Reliable structured outputs come from five habits: declare every property required (use null-unions for optionals), set additionalProperties: false on every object, prefer enums over free text, keep nesting shallow, and treat field descriptions as instructions. Then validate and retry with the errors — never trust, always check.
§1 · JSON mode is not structured output
"JSON mode" guarantees the response parses. It does not guarantee your field names, your types, or that status is one of the four values your database accepts. Structured outputs — schema-constrained generation — move that contract from the prompt (a request) into the API (an enforcement). The difference shows up at volume: a prompt-only pipeline that fails 2% of the time fails 20,000 times at a million calls.
There is also a cost angle: schema-constrained answers are consistently shorter than prose-wrapped ones, and output tokens cost 4–6× input tokens on every provider (see the July 2026 pricing table). The schema is a budget instrument, not just a correctness one.
§2 · The baseline recipe (works on all three providers)
Write the schema as if OpenAI strict mode were the target, even when it is not — it is the most demanding dialect, so it is the portable one:
- Every property in
required. Strict mode rejects schemas with genuinely optional fields. For "optional", use a null union:"type": ["string", "null"]— and treatnullas absent downstream. additionalProperties: falseat every object level, not just the root. The single most-forgotten rule and the top reason schemas fail strict-mode validation.- Enums over free text.
"enum": ["low", "medium", "high"]ends the "High / HIGH / élevé" cleanup forever. If you will ever switch on a value, it should be an enum. - Shallow beats deep. Two levels of nesting is comfortable; five is where models start losing keys. Flatten aggressively; join in your code, not in the model's head.
- Descriptions are instructions.
"description": "ISO 8601 date, UTC, no time component"does more than the same sentence buried in the system prompt, because it travels attached to the field being generated.
Strict-ready fragments
| Pattern | Schema fragment |
|---|---|
| Required + nullable | "notes": { "type": ["string","null"], "description": "null when none" } |
| Closed object | "additionalProperties": false — on EVERY object |
| Enum switch | "status": { "enum": ["queued","running","done","failed"] } |
| Typed number | "qty": { "type": "integer", "minimum": 0 } |
Build and export this shape in one click — strict-mode OpenAI or Anthropic tool_use — with the JSON Schema Builder.
§3 · Provider dialects, one paragraph each
- OpenAI (strict mode). Pass the schema with
strict: true; the API validates your schema up front and rejects it if any property is missing fromrequiredor any object allows additional properties. Painful once, then bulletproof. - Anthropic (tool_use). Define a tool whose
input_schemais standard JSON Schema (properties, required, types, enums) and force the tool call. The model fills the tool arguments — that IS your structured output. Strict-shaped schemas behave best here too. - Gemini (response schema). Accepts a schema object on the request that constrains the response shape (an OpenAPI-style subset — check the current docs for which keywords are honored). Same design rules apply: required everything, enums, shallow nesting.
§4 · The failure modes that survive — and the repair loop
Even schema-constrained pipelines see malformed output at the edges (long context, high temperature, truncation). The five classics, in observed order of frequency:
- Markdown fences around the JSON (
```json … ```) — strip before parsing. - Leading/trailing prose ("Here is the JSON you asked for:") — isolate the outermost
{…}or[…]block. - Trailing commas — tolerated by JS, fatal for JSON parsers.
- Truncation — output hit max tokens mid-object; auto-close brackets to salvage, then treat as a retry signal.
- Type drift —
"qty": 3.0where you declared integer, or an enum value with different casing.
The production pattern is a two-step loop: repair mechanically, then validate against the schema; on failure, retry once feeding the exact validation errors back to the model. The repair step resolves most incidents without a second API call — which, at the output prices in the July 2026 table, is the cheapest retry you will ever ship.
Test the loop on your own responses: paste raw model output + schema, get repairs and validation errors.
Open output validator →§5 · FAQ
What is the difference between JSON mode and structured outputs?
JSON mode guarantees the output parses; structured outputs guarantee it matches YOUR schema — names, types, enums, required keys. At production volume, only the second one is a contract.
How do I make a field optional in strict mode?
You don't — you make it nullable: "type": ["string","null"], keep it in required, and treat null as absent. This is the strict-mode idiom for optionality.
My schema is correct but responses still fail. Why?
Look for fences, surrounding prose, trailing commas, truncation (max tokens too low) and number-vs-integer drift. Run the repair step before validation; feed validation errors back on retry.
Does one schema work across GPT, Claude and Gemini?
Yes, if you write it to strict-mode rules — the most demanding dialect. Objects with everything required, additionalProperties: false, enums and shallow nesting behave well on all three.