Structured Output: Getting Reliable JSON from an LLM

A
Author
16 Aug, 2026 2 min read 892 views

Asking a model to "reply in JSON" works most of the time. Production code needs it to work every time. Structured outputs constrain the response to a JSON Schema, so the result always parses.

TypeScript with Zod

import Anthropic from '@anthropic-ai/sdk';
import { z } from 'zod';
import { zodOutputFormat } from '@anthropic-ai/sdk/helpers/zod';

const Ticket = z.object({
  category: z.enum(['billing', 'bug', 'feature', 'other']),
  priority: z.enum(['low', 'medium', 'high']),
  summary: z.string(),
  customer_email: z.string().nullable(),
});

const client = new Anthropic();

const response = await client.messages.parse({
  model: 'claude-opus-5-5',
  max_tokens: 16000,
  messages: [{ role: 'user', content: `Classify this ticket:\n\n${emailBody}` }],
  output_config: { format: zodOutputFormat(Ticket) },
});

const ticket = response.parsed_output; // typed as z.infer<typeof Ticket>, or null
if (ticket) await saveTicket(ticket);

Python with Pydantic

from typing import Literal, Optional
from pydantic import BaseModel
import anthropic

class Ticket(BaseModel):
    category: Literal["billing", "bug", "feature", "other"]
    priority: Literal["low", "medium", "high"]
    summary: str
    customer_email: Optional[str]

client = anthropic.Anthropic()

response = client.messages.parse(
    model="claude-opus-5-5",
    max_tokens=16000,
    messages=[{"role": "user", "content": f"Classify this ticket:\n\n{email_body}"}],
    output_format=Ticket,
)

ticket = response.parsed_output  # a validated Ticket instance

Raw JSON Schema

Without a helper library, pass the schema yourself in output_config.format and parse the text block:

const response = await client.messages.create({
  model: 'claude-opus-5-5',
  max_tokens: 16000,
  messages: [{ role: 'user', content: 'Extract the city and country from: "I live in Pune, India."' }],
  output_config: {
    format: {
      type: 'json_schema',
      schema: {
        type: 'object',
        properties: { city: { type: 'string' }, country: { type: 'string' } },
        required: ['city', 'country'],
        additionalProperties: false,
      },
    },
  },
});

const text = response.content.find((b) => b.type === 'text');
const place = JSON.parse(text.text);

Schema design tips

  • Use enum for categories so you never get "Bug", "bug " and "defect" for the same thing.
  • Use nullable fields for data that might be missing instead of letting the model invent values.
  • Add a short description to fields whose meaning isn't obvious from the name.
  • Keep schemas flat where you can; deeply nested schemas are harder to fill correctly.
Was this article helpful?

Comments

No comments yet. Have a question or a tip? Be the first to share it.

Leave a comment

Comments are reviewed before they appear.