aillmjsonapi

JSON Mode & Structured Outputs: OpenAI, Anthropic & Gemini Compared

·10 min read·AI & JSON

JSON mode vs structured outputs

These two terms get used interchangeably but mean different things:

  • JSON mode constrains the model to emit *syntactically valid JSON*. You are guaranteed it will parse, but not that it contains the keys you expect.
  • Structured outputs constrain the model to a *specific JSON Schema*. You are guaranteed both that it parses and that it matches your shape — correct keys, types, and enums.

If your downstream code depends on a fixed shape (and it almost always does), prefer structured outputs.

OpenAI

OpenAI offers both. Plain JSON mode:

json
{ "response_format": { "type": "json_object" } }

Schema-constrained Structured Outputs (recommended):

python
response_format={
  "type": "json_schema",
  "json_schema": {
    "name": "invoice",
    "strict": True,
    "schema": {
      "type": "object",
      "properties": {
        "invoice_number": {"type": "string"},
        "total": {"type": "number"},
        "currency": {"type": "string", "enum": ["USD", "EUR", "INR"]}
      },
      "required": ["invoice_number", "total", "currency"],
      "additionalProperties": False
    }
  }
}

With strict: true, the decoder is constrained so the output cannot violate the schema. Every property you want guaranteed must be in required, and additionalProperties must be false.

Anthropic (Claude)

Claude does not have a separate "JSON mode" toggle — instead you use tool use. Define a tool with an input_schema and force the model to call it:

python
tools=[{
  "name": "extract_invoice",
  "input_schema": {
    "type": "object",
    "properties": {
      "invoice_number": {"type": "string"},
      "total": {"type": "number"}
    },
    "required": ["invoice_number", "total"]
  }
}],
tool_choice={"type": "tool", "name": "extract_invoice"}

The model's response contains a tool_use block whose input is a JSON object matching the schema. This pattern doubles as your function-calling mechanism, so the same schema can both shape data and trigger code.

Google Gemini

Gemini accepts a response MIME type plus an optional schema:

python
generation_config={
  "response_mime_type": "application/json",
  "response_schema": {
    "type": "object",
    "properties": {
      "invoice_number": {"type": "string"},
      "total": {"type": "number"}
    },
    "required": ["invoice_number", "total"]
  }
}

Feature comparison

CapabilityOpenAIClaudeGemini
Plain valid-JSON modeYesVia toolsYes
Schema-constrained outputYes (strict)Yes (tool input_schema)Yes (response_schema)
Enums enforcedYesYesYes
Doubles as function callingSeparateYes (same mechanism)Separate
Nested objectsYesYesYes

Practical trade-offs

  • Strictness costs flexibility. A rigid schema is great for ingestion but will make the model drop information that does not fit. If you want the model to also explain its reasoning, add a notes string field rather than loosening the schema.
  • Keep schemas shallow. Three or four levels of nesting is fine; ten is asking for trouble. Flatten where you can.
  • Always validate anyway. Provider guarantees are strong but not a substitute for validating in your own code, especially across retries and model upgrades.

Frequently asked questions

If you are on OpenAI, use Structured Outputs with strict: true. On Claude, use tool use with an input_schema. On Gemini, set response_schema. All three give you schema-valid JSON.

Mostly. The core JSON Schema (types, properties, required, enum) is portable. Each provider has minor constraints — for example OpenAI strict mode requires additionalProperties: false and every property in required.

The overhead is small and usually worth it. The bigger cost is design time spent getting the schema right.

Try JSON Schema Validator

Validate model output against your JSON Schema with full error details.