JSON Schema for LLM structured output: the developer's shortcut
OpenAI's structured output and Anthropic's tool use both want JSON Schema. Here's the draft that works everywhere, the fields models care about, and how to write one from an example in 10 seconds.
Why JSON Schema is having a moment
Three things happened in the last 18 months that made JSON Schema everyone's problem:
- OpenAI's structured output (GPT-4o) accepts a JSON Schema and guarantees the model returns matching JSON. No more parsing failures, no more
try/catcharound every LLM call. - Anthropic's tool use (Claude 3+) takes a JSON Schema per tool. The model uses it to plan calls.
- MCP (Model Context Protocol) โ every tool exposed to Claude Code, ChatGPT, and Copilot ships a JSON Schema for its inputs.
If you're building anything with LLMs beyond a chat wrapper, you're writing JSON schemas. Often lots of them.
The draft that matters: 2020-12
There are eight active JSON Schema drafts. Only one matters for LLM work: draft 2020-12. That's what OpenAI, Anthropic, and MCP all expect.
The $schema line at the top:
{
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
Older drafts (draft-07, draft-04) work in some libraries but fail model validators. Newer drafts don't exist yet. Just use 2020-12.
The five keywords the models actually care about
Not every JSON Schema feature is respected by LLM providers. The reliable subset:
type โ "string", "number", "integer", "boolean", "object", "array", or "null". Union types (["string", "null"]) are supported for nullable fields.
properties โ for objects. Every field's name and its own schema.
required โ an array of field names that must appear. OpenAI structured output makes every field required by default; use type: ["X", "null"] for optional fields.
enum โ a list of allowed values. Extremely useful for constraining LLM output ("choose one of these categories").
description โ free text explaining what the field means. The model reads this and uses it. A good description ("The user's ISO 3166-1 alpha-2 country code") is often more effective than a strict format constraint.
Keywords like format, pattern, minLength, and $ref work in some providers and are ignored in others. Test before relying on them.
The OpenAI strict-mode rules
OpenAI's strict structured output has additional constraints:
- All object properties must be in
required. No optional fields. Use nullable types instead. additionalProperties: falseon every object. The model can't invent new fields.- Root must be an object. Not an array, not a primitive. Wrap arrays as
{ items: [...] }. - No
anyOf/oneOfat the root. Use inside properties, not at top level.
If you get Invalid schema errors, it's almost always one of these four rules. Anthropic's tool use is more permissive.
Building a schema from an example
The fastest workflow for any new LLM integration:
- Write a sample of the ideal response as plain JSON.
- Generate the schema with JSON Schema Generator โ it infers types, marks fields required, handles nested objects and arrays.
- Add descriptions to each field โ this is where the model gets its instructions.
- Test with 5-10 diverse inputs โ LLMs surface schema issues that a single test doesn't.
- Iterate โ tighten enums, relax required lists based on real behavior.
Step 1 is the one people skip and regret. A concrete example beats an abstract schema every time.
Schema vs prompt: where to put constraints
You have two places to constrain LLM output: the schema and the prompt. When to use which:
Put in the schema:
- Field names, types, structure
- Enums (list of allowed values)
- Nesting and required/optional
Put in the prompt (as description) or the system message:
- Semantic rules ("summary must be under 100 words")
- Cross-field logic ("if type is 'error', message is required")
- Formatting preferences ("dates in ISO 8601")
Rule of thumb: the schema enforces shape; the prompt enforces meaning.
The TypeScript connection
If your codebase is TypeScript, you have two options:
Schema โ TypeScript. Write the JSON Schema, generate types with json-schema-to-typescript. Types stay in sync with what the LLM produces.
TypeScript โ Schema. Write Zod schemas, use zod-to-json-schema to send to the LLM. Same source of truth, better DX.
Both are valid. The one to avoid: hand-written TS types plus hand-written JSON schemas that drift apart. Pick one source, generate the other.
If you're moving from LLM outputs to TypeScript types quickly, JSON to TypeScript generates interfaces from a sample response โ useful for typing the consumer of an LLM-generated JSON payload.
Debugging failed schema validations
When strict output fails, the error message is almost always vague. The workflow:
- Format the schema to eyeball it.
- Check the five OpenAI rules above โ 80% of failures are one of them.
- Minify the schema โ remove one field at a time until it passes, then you know which field is the problem.
- Compare against a working schema with JSON Diff โ structural diff catches subtle key-order or nesting issues.
Related workflows
- LLM Token Counter โ schemas count toward input tokens; know the cost.
- JSON to TypeScript โ for typing the response you get back.
- JSON Formatter โ for eyeballing schema and response side by side.
Tools mentioned in this post
Related reading
LLM token counting and API cost estimation: a 2026 developer's guide
Tokens aren't words, GPT-4o and Claude count them differently, and a 1M-context prompt can cost $30. Here's how tokens actually work, and how to estimate cost before you ship.
JSON to TypeScript: workflows that scale beyond a single sample
A one-shot JSON โ interface tool is great for demos and dead for production. Here are the patterns real teams use to keep types in sync with real APIs.
HTTP status codes that actually matter for API developers in 2026
You know 200 and 500. But the difference between 401 vs 403, 400 vs 422, and 502 vs 503 decides whether your API is professional or amateur. Here's the short list that matters.
Markdown flavors explained: CommonMark, GFM, MDX, and why your table doesn't render
Markdown looks universal until you paste a GitHub table into a Discord post. Here's what each flavor supports, and how to pick one for your project.
Shan builds 712 Tools. He holds a Master's degree in Mechanical Engineering and now works as a Software Engineer, shipping browser-based developer utilities out of Ontario, Canada. Learn more ยท 712studiogames@gmail.com