How do you get structured JSON output from an LLM?

Use response_format with json_object or json_schema mode to force valid JSON output from an LLM. Pair it with a clear schema in the system prompt and TypeScript types for safe parsing.

AILLM

Overview

By default LLMs generate free-form text. For any application that needs to parse the response programmatically (filling a form, inserting into a database, rendering UI), you need structured output. Modern LLM APIs offer built-in JSON mode and strict schema enforcement so you never have to regex-parse markdown code fences again.

Key Points

  • •OpenAI json_object mode guarantees valid JSON but you still describe the shape in the prompt.
  • •OpenAI json_schema mode (Structured Outputs) validates the response against a JSON Schema you provide - zero invalid responses.
  • •Anthropic Claude produces reliable JSON when you end the system prompt with the opening brace `{` to prime the output.
  • •Always parse with try/catch - even json mode can produce valid JSON with unexpected structure.
  • •Zod is the standard TypeScript library for validating and typing parsed JSON at runtime.
  • •Set temperature to 0 when extracting structured data - consistency matters more than creativity here.

Examples

OpenAI json_object mode (basic)

typescript
const response = await client.chat.completions.create({
  model: 'gpt-4o',
  temperature: 0,
  response_format: { type: 'json_object' },
  messages: [
    {
      role: 'system',
      content: `Extract the following fields from the user bio and return valid JSON:
{ "name": string, "company": string, "role": string, "yearsOfExperience": number }
If a field is not mentioned set it to null.`,
    },
    {
      role: 'user',
      content: 'Alice Johnson is a senior backend engineer at Stripe with 8 years of experience.',
    },
  ],
});

const data = JSON.parse(response.choices[0].message.content!);
// { name: "Alice Johnson", company: "Stripe", role: "senior backend engineer", yearsOfExperience: 8 }

OpenAI Structured Outputs with json_schema (strict mode)

typescript
const response = await client.chat.completions.create({
  model: 'gpt-4o-2024-08-06',  // must support structured outputs
  messages: [{ role: 'user', content: 'Analyse the sentiment of: "This product is amazing!"' }],
  response_format: {
    type: 'json_schema',
    json_schema: {
      name: 'sentiment_result',
      strict: true,
      schema: {
        type: 'object',
        properties: {
          sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'] },
          confidence: { type: 'number' },
          reasoning:  { type: 'string' },
        },
        required: ['sentiment', 'confidence', 'reasoning'],
        additionalProperties: false,
      },
    },
  },
});

// OpenAI guarantees the response always matches the schema
const result = JSON.parse(response.choices[0].message.content!);
// { sentiment: "positive", confidence: 0.98, reasoning: "..." }

Validate with Zod for type-safe parsing

typescript
import { z } from 'zod';

const PersonSchema = z.object({
  name:              z.string(),
  email:             z.string().email().nullable(),
  yearsOfExperience: z.number().min(0).max(50),
  skills:            z.array(z.string()),
});

type Person = z.infer<typeof PersonSchema>;

async function extractPerson(bio: string): Promise<Person> {
  const response = await client.chat.completions.create({
    model: 'gpt-4o',
    temperature: 0,
    response_format: { type: 'json_object' },
    messages: [
      { role: 'system', content: 'Extract person info as JSON matching this shape: ' + JSON.stringify(PersonSchema.shape) },
      { role: 'user',   content: bio },
    ],
  });

  const raw = JSON.parse(response.choices[0].message.content!);
  return PersonSchema.parse(raw); // throws ZodError if shape is wrong
}

Anthropic Claude structured output trick

typescript
// Claude produces reliable JSON when the assistant turn starts with {
const response = await client.messages.create({
  model: 'claude-sonnet-4-6',
  max_tokens: 1024,
  system: 'You extract structured data. Respond only with valid JSON.',
  messages: [
    { role: 'user',      content: 'Extract name and role from: "Bob is a DevOps engineer."' },
    { role: 'assistant', content: '{' },  // prime the model to start with JSON
  ],
});

// Prepend the { we injected above
const json = '{' + response.content[0].text;
const data = JSON.parse(json);

Pro Tip

Define your schema once and reuse it for both the LLM prompt and the Zod validator. That way the model knows exactly what shape to produce and your TypeScript code has a strongly-typed result with zero runtime surprises.