Lesson 2 of 3 · Structured Outputs & Reliable AI

JSON Mode vs Structured Outputs

reading12 min

A team migrating from JSON Mode to Structured Outputs discovered the distinction the hard way. Their extraction pipeline used response_format: { type: "json_object" } -- JSON Mode -- which guaranteed valid JSON but nothing else. The model returned valid JSON every time, but the shape of that JSON was unpredictable. Sometimes the array field was called items. Sometimes it was called results. Sometimes the price was a number; sometimes it was a string with a dollar sign. JSON Mode solved one problem (valid JSON) while leaving the harder problem (schema adherence) completely unsolved.

Understanding the precise difference between JSON Mode and Structured Outputs is essential because they solve different problems, and using the wrong one gives you a false sense of security.

JSON Mode: Valid JSON, Unknown Shape

JSON Mode was OpenAI's first attempt at reliable output formatting. It constrains the model to produce valid JSON -- proper syntax, matched brackets, correct quoting. Nothing more.

typescript
// JSON Mode
const response = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [
    {
      role: "user",
      content: "Extract the person's name and age from: 'John Smith is 34 years old'"
    }
  ],
  response_format: { type: "json_object" }
});

// Guaranteed: valid JSON
// NOT guaranteed: specific fields, types, or structure
// Could return:
// {"name": "John Smith", "age": 34}
// {"person": {"name": "John Smith", "age": "34"}}
// {"result": "John Smith, age 34"}
// All are valid JSON. None are guaranteed to match your code.
Concept Card

Structured Outputs: Valid JSON + Schema Adherence

Structured Outputs extend JSON Mode with a critical addition: a JSON Schema that the model must follow. The output is not just valid JSON -- it matches your exact schema.

typescript
// Structured Outputs
const response = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [
    {
      role: "user",
      content: "Extract the person's name and age from: 'John Smith is 34 years old'"
    }
  ],
  response_format: {
    type: "json_schema",
    json_schema: {
      name: "person_extraction",
      strict: true,
      schema: {
        type: "object",
        properties: {
          name: { type: "string" },
          age: { type: "number" }
        },
        required: ["name", "age"],
        additionalProperties: false
      }
    }
  }
});

// Guaranteed: valid JSON
// Guaranteed: has "name" (string) and "age" (number)
// Guaranteed: no extra fields
// Always returns: {"name": "John Smith", "age": 34}

The Key Differences

FeatureJSON ModeStructured Outputs
Valid JSON syntaxYesYes
Specific field namesNoYes
Type enforcementNoYes
Required fieldsNoYes
No extra fieldsNoYes (with additionalProperties: false)
Enum constraintsNoYes
Nested object schemasNoYes
Array item schemasNoYes
Schema versioningN/AYes (via schema name)
Refusal handlingNoYes

When to Use JSON Mode

JSON Mode still has valid use cases, though they are narrower than most developers think:

When to Use Structured Outputs

For almost everything in production:

  • Any pipeline where downstream code expects specific fields and types
  • API responses that feed into typed application code
  • Data extraction that inserts into a database
  • Classification systems where categories are predefined
  • Any system where output format errors have business consequences
Default to Structured Outputs

If you are starting a new project today, default to Structured Outputs for every API call that returns data your application processes programmatically. Use JSON Mode only during early prototyping when you do not yet know your schema. Then migrate to Structured Outputs as soon as your schema stabilizes.

Migration Path: JSON Mode to Structured Outputs

If you have an existing system using JSON Mode, migration is straightforward:

typescript
// Before: JSON Mode
const before = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: prompt }],
  response_format: { type: "json_object" }
});
// Then: manual parsing, validation, type coercion
const data = JSON.parse(before.choices[0].message.content!);
if (!data.name || typeof data.age !== "number") {
  throw new Error("Invalid response format");
}

// After: Structured Outputs
const after = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: prompt }],
  response_format: {
    type: "json_schema",
    json_schema: {
      name: "person",
      strict: true,
      schema: {
        type: "object",
        properties: {
          name: { type: "string" },
          age: { type: "number" }
        },
        required: ["name", "age"],
        additionalProperties: false
      }
    }
  }
});
// No validation needed -- schema is enforced
const data = JSON.parse(after.choices[0].message.content!);
// data.name is always a string, data.age is always a number
Do

Migrate existing JSON Mode pipelines to Structured Outputs incrementally. Start with the endpoints that have the most parsing failures or the most complex validation logic -- those benefit most. Keep your existing validation as a safety net during migration, then remove it once you have confirmed Structured Outputs handles all cases.

Don't

Remove all validation logic immediately after switching to Structured Outputs. Keep it as a logging-only safety net for a few weeks to verify that the structured output matches what your validation was checking. Once you have confidence, remove the redundant validation to simplify your code.

The Six Date Parsers

beginner

You have an existing pipeline using JSON Mode that extracts product information from e-commerce listings. The model returns valid JSON, but field names vary ("price" vs "cost" vs "amount"), types fluctuate ("29.99" vs 29.99), and optional fields sometimes appear as empty strings instead of being omitted. Write the migration plan: (1) Define the target JSON Schema with strict: true. (2) List all the validation code you can remove after migration. (3) Identify any edge cases where the structured output might behave differently than your current parsing. (4) Design the rollout strategy -- would you switch all at once or run both in parallel?