Back to Blog
    September 30, 20269 min read

    LLM Structured Outputs 2026: Get Perfect JSON from ChatGPT, Claude & Gemini

    AI
    LLM
    JSON Schema
    Structured Output

    Getting a language model to return well-formed, schema-valid JSON used to be a gamble. In 2026, every major provider ships a native structured-output mode backed by grammar-constrained decoding — the model cannot physically emit a token that would break your schema. Here is the definitive guide to using it across OpenAI, Anthropic Claude, and Google Gemini.

    Why Unstructured LLM Output Breaks Pipelines

    Before structured outputs existed, developers bolted JSON extraction on top of free-text generation. The model would often add a markdown code fence, an apologetic preamble, or truncate mid-object when it hit the token limit. Even with careful prompting you would see output like:

    Sure! Here is the JSON you asked for:
    ```json
    {
      "name": "Alice",
      "score": 42,
      // Note: higher is better
    }
    ```

    That response fails JSON.parse() on three counts: the prose, the code fence, and the trailing comment. Production code handling this had to strip fences with regex, retry on parse failure, and still crash unpredictably. Structured outputs eliminate all of that.

    How Grammar-Constrained Decoding Works

    At inference time the provider compiles your JSON Schema into a finite-state automaton. At every token position the sampler masks out any token whose inclusion would put the output into an invalid state. The model never "chooses" to add a trailing comma or drop a closing brace — those tokens are simply invisible to it during sampling.

    The result is a guarantee, not a probability: if your schema says score is an integer between 0 and 100, the output will always be a valid integer in that range. No retries, no regex cleanup.

    Validate the output anyway

    Grammar constraints guarantee syntactic validity against the schema structure, but they cannot enforce business logic (e.g. “end_date must be after start_date”). Always run Pydantic or Zod on the parsed object for semantic validation. Use our JSON Validator to spot-check schemas during development.

    OpenAI: response_format with Strict Mode

    OpenAI introduced response_format: { type: 'json_schema' } with strict: true in late 2024. All GPT-4o and o-series models support it as of 2026. The schema must be a subset of JSON Schema draft-07 (no$ref recursion, no anyOf at the top level).

    import OpenAI from 'openai';
    
    const client = new OpenAI();
    
    const response = await client.chat.completions.create({
      model: 'gpt-4o',
      messages: [
        { role: 'user', content: 'Extract the product name and price from: "Blue Widget $14.99"' }
      ],
      response_format: {
        type: 'json_schema',
        json_schema: {
          name: 'product_extraction',
          strict: true,
          schema: {
            type: 'object',
            properties: {
              name:  { type: 'string' },
              price: { type: 'number' }
            },
            required: ['name', 'price'],
            additionalProperties: false
          }
        }
      }
    });
    
    const product = JSON.parse(response.choices[0].message.content!);
    // { name: 'Blue Widget', price: 14.99 }  — guaranteed valid

    With the Python SDK you can pass a Pydantic model directly and the SDK compiles the schema for you:

    from openai import OpenAI
    from pydantic import BaseModel
    
    class Product(BaseModel):
        name: str
        price: float
    
    client = OpenAI()
    completion = client.beta.chat.completions.parse(
        model='gpt-4o',
        messages=[{'role': 'user', 'content': 'Extract: "Blue Widget $14.99"'}],
        response_format=Product,
    )
    product = completion.choices[0].message.parsed
    # Product(name='Blue Widget', price=14.99)

    Anthropic Claude: Tool Use as Structured Output

    Claude does not expose a response_format field. Instead you define a tool whose input schema is your desired JSON shape, then instruct the model to call it. The API enforces the schema the same way it enforces tool-call arguments — grammar-constrained at the token level. Use tool_choice: { type: 'tool', name: '...' } to force the call.

    import Anthropic from '@anthropic-ai/sdk';
    
    const client = new Anthropic();
    
    const response = await client.messages.create({
      model: 'claude-opus-5-5',
      max_tokens: 256,
      tools: [{
        name: 'extract_product',
        description: 'Extract product details from text',
        input_schema: {
          type: 'object',
          properties: {
            name:  { type: 'string' },
            price: { type: 'number' }
          },
          required: ['name', 'price']
        }
      }],
      tool_choice: { type: 'tool', name: 'extract_product' },
      messages: [{
        role: 'user',
        content: 'Extract: "Blue Widget $14.99"'
      }]
    });
    
    const toolUse = response.content.find(b => b.type === 'tool_use');
    const product = toolUse?.input;
    // { name: 'Blue Widget', price: 14.99 }

    The Instructor library wraps this pattern for both Python and TypeScript, letting you pass Pydantic/Zod models directly:

    import Instructor from '@instructor-ai/instructor';
    import Anthropic from '@anthropic-ai/sdk';
    import { z } from 'zod';
    
    const client = Instructor({ client: new Anthropic(), mode: 'TOOLS' });
    
    const Product = z.object({ name: z.string(), price: z.number() });
    
    const product = await client.chat.completions.create({
      model: 'claude-opus-5-5',
      max_tokens: 256,
      messages: [{ role: 'user', content: 'Extract: "Blue Widget $14.99"' }],
      response_model: { schema: Product, name: 'Product' }
    });
    // { name: 'Blue Widget', price: 14.99 }

    Google Gemini: response_mime_type + response_schema

    The Gemini API uses response_mime_type: "application/json" together with response_schema. Both Gemini 1.5 Pro and the Gemini 2.x family support it. Pass a JSON Schema object or a Python type annotation via the Google genai SDK.

    import { GoogleGenerativeAI, SchemaType } from '@google/generative-ai';
    
    const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY!);
    const model = genAI.getGenerativeModel({
      model: 'gemini-2.0-flash',
      generationConfig: {
        responseMimeType: 'application/json',
        responseSchema: {
          type: SchemaType.OBJECT,
          properties: {
            name:  { type: SchemaType.STRING },
            price: { type: SchemaType.NUMBER }
          },
          required: ['name', 'price']
        }
      }
    });
    
    const result = await model.generateContent('Extract: "Blue Widget $14.99"');
    const product = JSON.parse(result.response.text());
    // { name: 'Blue Widget', price: 14.99 }

    Schema Validation: Pydantic v2 (Python) and Zod (TypeScript)

    Grammar constraints make the JSON syntactically valid and structurally correct. A second validation layer catches semantic problems: out-of-range numbers, wrong enum values, business-logic violations.

    Pydantic v2

    from pydantic import BaseModel, Field, field_validator
    from typing import Literal
    
    class SentimentResult(BaseModel):
        sentiment: Literal['positive', 'neutral', 'negative']
        confidence: float = Field(ge=0.0, le=1.0)
        summary: str = Field(max_length=200)
    
        @field_validator('summary')
        @classmethod
        def no_empty_summary(cls, v: str) -> str:
            if not v.strip():
                raise ValueError('summary must not be blank')
            return v
    
    # After parsing the LLM response:
    result = SentimentResult.model_validate(raw_dict)
    # Raises ValidationError with clear messages if anything is wrong

    Zod (TypeScript)

    import { z } from 'zod';
    
    const SentimentResult = z.object({
      sentiment: z.enum(['positive', 'neutral', 'negative']),
      confidence: z.number().min(0).max(1),
      summary: z.string().min(1).max(200),
    });
    
    type SentimentResult = z.infer<typeof SentimentResult>;
    
    // After parsing:
    const result = SentimentResult.parse(rawObject);
    // Throws ZodError with path + message if validation fails

    Common pitfall: deeply nested schemas

    Grammar-constrained decoding slows down with deeply nested schemas because the automaton grows exponentially. Keep schemas flat where possible, or split one large structured call into two smaller ones. If you need to inspect a complex schema before sending it to an LLM, our JSON Schema Validator will catch structural errors before they confuse the model.

    Provider Comparison at a Glance

    ProviderMechanismSchema formatConstraint type
    OpenAI GPT-4oresponse_format.json_schemaJSON Schema draft-07 subsetGrammar (strict mode)
    Anthropic ClaudeTool use / tool_choiceJSON Schema (tool input)Grammar (tool args)
    Google Geminiresponse_mime_typeGemini SchemaType / JSON SchemaGrammar

    Formatting and Debugging the Output

    Even with structured outputs you will occasionally need to inspect a raw response — especially during prompt engineering when the schema is still evolving. Our JSON Formatter lets you paste the raw string and immediately see a color-coded, collapsible tree so you can verify the shape before wiring it into your validation layer.

    If you are iterating on the schema itself, the JSON Schema Validator will validate a sample response against your draft schema and highlight every path that fails — far faster than running the full LLM call each time.

    Tips for Production-Ready Structured Output Pipelines

    • Pin the schema version. Store your JSON Schema in a versioned file. Any change to the schema is a breaking change for consumers — treat it like a database migration.
    • Set additionalProperties: false. This prevents the model from hallucinating extra fields. OpenAI strict mode requires it; add it yourself for Claude and Gemini.
    • Limit array lengths. Use maxItems to prevent unbounded arrays that bloat your token bill and slow grammar-constrained sampling.
    • Add descriptions to every property. Grammar constraints handle syntax; descriptions handle semantics. A field named ts with description "Unix timestamp in milliseconds" is less likely to be mis-populated than a bare ts: number.
    • Handle refusals. OpenAI can still return a refusal in the response content for policy reasons even with strict mode on. Check message.refusal before accessing message.content.

    Try These Free Tools

    Everything covered in this guide — formatting raw LLM responses, validating JSON structure, and checking output against a JSON Schema — is available for free, right in your browser. No login, no install.

    Conclusion

    Structured outputs in 2026 have closed the gap between "LLM returns text" and "LLM returns a typed data structure". The grammar-constrained decoding layer across OpenAI, Claude, and Gemini means you can build reliable data-extraction, classification, and API-generation pipelines without defensive retry loops.

    The remaining responsibility is yours: write precise schemas, add semantic validation with Pydantic or Zod, and keep your schemas versioned. When something looks wrong, paste the output into the JSON Formatter or the JSON Validator to diagnose it immediately — no LLM call needed.