Lesson 3 of 3 · Structured Outputs & Reliable AI

JSON Schema for AI

reading12 min

JSON Schema is a vocabulary for describing the structure of JSON data. It has been around since 2009, used primarily for API documentation and request validation. Structured Outputs repurposes it as a constraint language for AI model output -- and that change in context matters, because some JSON Schema features work differently when they are constraining a generative model versus validating a static document.

The Basics: Types and Properties

Every schema starts with a type and, for objects, a set of properties:

JSON
{
  "type": "object",
  "properties": {
    "title": { "type": "string" },
    "rating": { "type": "number" },
    "published": { "type": "boolean" }
  },
  "required": ["title", "rating", "published"],
  "additionalProperties": false
}
Two Critical Requirements for Strict Mode

When using strict: true (which you should always use), two requirements apply that are not typical in standard JSON Schema usage: (1) Every property must be listed in required. (2) additionalProperties must be set to false. These are enforced -- your schema will be rejected if they are missing.

Supported Types

Structured Outputs supports the core JSON Schema types with some AI-specific considerations:

TypeExampleAI Behavior Notes
string"hello"The model generates free-form text within the field
number42, 3.14Integers and floats; the model chooses based on context
booleantrue, falseReliable for binary classification tasks
nullnullUsed in union types for optional values
object{"a": 1}Nested objects with their own property schemas
array[1, 2, 3]Items schema defines the type of each element

Enum Constraints

Enums are one of the most powerful features for AI output because they eliminate an entire class of parsing problems: value normalization.

JSON
{
  "type": "object",
  "properties": {
    "sentiment": {
      "type": "string",
      "enum": ["positive", "negative", "neutral", "mixed"]
    },
    "confidence": {
      "type": "string",
      "enum": ["high", "medium", "low"]
    }
  },
  "required": ["sentiment", "confidence"],
  "additionalProperties": false
}

Without enums, the model might return "Positive", "POSITIVE", "pos", "mostly positive", or "I would say positive." With enums, it returns exactly one of the four specified values. Every time.

Do

Use enums for any field with a fixed set of valid values: categories, status codes, severity levels, rating scales, classification labels. Enums eliminate value normalization entirely and make downstream aggregation trivial.

Don't

Create enums with 50+ values or values that are ambiguously similar ('good' vs 'mostly_good' vs 'fairly_good'). The model may struggle to distinguish between very similar enum values, especially when the input is ambiguous. Keep enum sets clear and mutually exclusive.

Nested Objects

Real-world data is rarely flat. Structured Outputs handles nested objects naturally:

JSON
{
  "type": "object",
  "properties": {
    "customer": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "email": { "type": "string" },
        "address": {
          "type": "object",
          "properties": {
            "street": { "type": "string" },
            "city": { "type": "string" },
            "state": { "type": "string" },
            "zip": { "type": "string" }
          },
          "required": ["street", "city", "state", "zip"],
          "additionalProperties": false
        }
      },
      "required": ["name", "email", "address"],
      "additionalProperties": false
    },
    "order_total": { "type": "number" }
  },
  "required": ["customer", "order_total"],
  "additionalProperties": false
}

Every nested object needs its own required and additionalProperties: false in strict mode. This is verbose but explicit -- there is no ambiguity about what the model must produce.

Arrays with Typed Items

Arrays are essential for extraction tasks that produce variable-length results:

JSON
{
  "type": "object",
  "properties": {
    "line_items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "description": { "type": "string" },
          "quantity": { "type": "number" },
          "unit_price": { "type": "number" },
          "total": { "type": "number" }
        },
        "required": ["description", "quantity", "unit_price", "total"],
        "additionalProperties": false
      }
    }
  },
  "required": ["line_items"],
  "additionalProperties": false
}

The model will return an array of objects, each matching the items schema. The array can be empty if no items are found -- which is valid and often preferable to the model inventing items.

Handling Optional Fields: The Union Pattern

In strict mode, all properties must be required. So how do you handle fields that may not exist in the source data? Use a union type with null:

JSON
{
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "phone": { "type": ["string", "null"] },
    "email": { "type": ["string", "null"] }
  },
  "required": ["name", "phone", "email"],
  "additionalProperties": false
}

The field phone is always present in the output (it is required), but its value can be either a string or null. This is how you tell the model: "this field exists in the schema, but if you cannot find the data, return null instead of making something up."

Concept Card

The Description Field: Guiding Model Behavior

JSON Schema's description field becomes a powerful steering tool when used with AI models. It does not affect schema validation, but it directly influences what the model generates:

JSON
{
  "type": "object",
  "properties": {
    "summary": {
      "type": "string",
      "description": "A 2-3 sentence summary of the document's main argument. Do not include specific data points -- focus on the thesis."
    },
    "word_count": {
      "type": "number",
      "description": "Estimated word count of the original document, rounded to the nearest hundred"
    },
    "publication_date": {
      "type": ["string", "null"],
      "description": "Publication date in ISO 8601 format (YYYY-MM-DD). Null if not mentioned in the document."
    }
  },
  "required": ["summary", "word_count", "publication_date"],
  "additionalProperties": false
}
Descriptions Are Inline Prompts

Treat the description field as an inline prompt for each field. You can specify format ("ISO 8601"), length constraints ("2-3 sentences"), behavioral rules ("return null if not found"), and domain guidance ("use medical terminology"). These descriptions are processed by the model alongside its schema constraints, giving you fine-grained control over each field's content.

Schema Limitations to Know

Structured Outputs supports a subset of JSON Schema. Key limitations:

  • No minLength / maxLength: You cannot enforce string length at the schema level. Use the description field to guide length.
  • No minimum / maximum: You cannot enforce number ranges. Use description for guidance.
  • No pattern: Regex patterns are not supported. The model follows format hints in description.
  • No if / then / else: Conditional schemas are not supported. Use discriminated unions (anyOf) instead.
  • Max nesting depth: 5 levels of object nesting.
  • Max properties per schema: 100 total across all levels.

beginner

Design a JSON Schema for extracting information from a job posting. Include: job title, company name, location (city, state, remote options), salary range (minimum and maximum, either or both may be missing), required skills (array of strings), experience level (enum: entry, mid, senior, lead, executive), and posting date. Handle all optional fields with union types. Add descriptions that guide the model to extract data accurately. Test your schema mentally against a job posting that lists only "competitive salary" with no numbers -- does your schema handle that gracefully?