712Tools
5 min read

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.

By
Software Engineer ยท M.Sc. Mechanical Engineering ยท Ontario, Canada

Why JSON Schema is having a moment

Three things happened in the last 18 months that made JSON Schema everyone's problem:

  1. OpenAI's structured output (GPT-4o) accepts a JSON Schema and guarantees the model returns matching JSON. No more parsing failures, no more try/catch around every LLM call.
  2. Anthropic's tool use (Claude 3+) takes a JSON Schema per tool. The model uses it to plan calls.
  3. 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: false on 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/oneOf at 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:

  1. Write a sample of the ideal response as plain JSON.
  2. Generate the schema with JSON Schema Generator โ€” it infers types, marks fields required, handles nested objects and arrays.
  3. Add descriptions to each field โ€” this is where the model gets its instructions.
  4. Test with 5-10 diverse inputs โ€” LLMs surface schema issues that a single test doesn't.
  5. 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:

  1. Format the schema to eyeball it.
  2. Check the five OpenAI rules above โ€” 80% of failures are one of them.
  3. Minify the schema โ€” remove one field at a time until it passes, then you know which field is the problem.
  4. Compare against a working schema with JSON Diff โ€” structural diff catches subtle key-order or nesting issues.

Related workflows

Tools mentioned in this post

Written by Shan

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