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:
{ "response_format": { "type": "json_object" } }Schema-constrained Structured Outputs (recommended):
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:
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:
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
| Capability | OpenAI | Claude | Gemini |
|---|---|---|---|
| Plain valid-JSON mode | Yes | Via tools | Yes |
| Schema-constrained output | Yes (strict) | Yes (tool input_schema) | Yes (response_schema) |
| Enums enforced | Yes | Yes | Yes |
| Doubles as function calling | Separate | Yes (same mechanism) | Separate |
| Nested objects | Yes | Yes | Yes |
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
notesstring 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.