Structured output (Gemini)
Gemini’s structured-output feature constrains generation so the output is guaranteed to be valid JSON matching a schema you provide. The mechanism is constrained decoding — the model’s sampling is restricted at each step to tokens that keep the output on a valid schema-completion path. The API surface is two parameters on the GenerateContentConfig: response_mime_type="application/json" (or "text/x.enum" for single-enum classification) and response_schema=... (Pydantic model, Zod schema, or raw JSON-Schema-subset dict). This is distinct from function calling — structured output gives you a JSON payload, function calling routes execution; they can be combined but solve different problems.
See also
- tool-use-and-function-calling-gemini — sibling feature; the same schema dialect
- gemini-api-and-sdks — surface methods called from here
- hidden-tricks-and-gotchas-gemini — response-MIME quirks
- tool-use-and-function-calling — Claude’s equivalent (strict mode in tools)
- structured-outputs-openai — OpenAI’s equivalent (response_format json_schema)
1. Three modes
Per ai.google.dev/gemini-api/docs/structured-output:
| Mode | When to use |
|---|---|
JSON mode (response_mime_type="application/json" + response_schema) | Structured data extraction, API-shaped responses |
Enum mode (response_mime_type="text/x.enum" + response_schema enum) | Single-value classification |
Free JSON (response_mime_type="application/json" only, no schema) | Loosely-structured JSON, model picks the shape |
2. JSON mode with Pydantic (Python)
The canonical pattern. Pass a Pydantic model and the SDK serializes it to the schema for you.
from pydantic import BaseModel, Field
from google import genai
from google.genai import types
class Recipe(BaseModel):
name: str = Field(description="Name of the dish")
ingredients: list[str] = Field(description="List of ingredients")
prep_minutes: int = Field(description="Prep time in minutes", ge=0)
difficulty: str = Field(description="One of: easy, medium, hard")
client = genai.Client(api_key="...")
resp = client.models.generate_content(
model="gemini-3.5-flash",
contents="Give me a recipe for chocolate chip cookies.",
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=Recipe,
),
)
# Two ways to get the result:
print(resp.text) # JSON string
recipe: Recipe = resp.parsed # Pydantic-validated object
print(recipe.name, recipe.prep_minutes)resp.parsed returns the already-validated Pydantic instance. If you don’t pass a Pydantic class, resp.parsed is a dict.
Lists of objects
class Recipe(BaseModel):
name: str
ingredients: list[str]
# Ask for multiple recipes
resp = client.models.generate_content(
model="gemini-3.5-flash",
contents="Three quick weeknight dinners.",
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=list[Recipe], # generic List[T]
),
)
recipes: list[Recipe] = resp.parsed3. JSON mode with raw schema dict
For non-Python or when you don’t want Pydantic:
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema={
"type": "OBJECT",
"properties": {
"name": {"type": "STRING", "description": "Dish name"},
"ingredients": {
"type": "ARRAY",
"items": {"type": "STRING"},
},
"prep_minutes": {"type": "INTEGER", "minimum": 0},
"difficulty": {
"type": "STRING",
"enum": ["easy", "medium", "hard"],
},
},
"required": ["name", "ingredients", "prep_minutes", "difficulty"],
"propertyOrdering": ["name", "ingredients", "prep_minutes", "difficulty"],
},
)Note the uppercase types (STRING not string) — same as function-calling schemas.
4. JSON mode with Zod (TypeScript)
import { GoogleGenAI } from "@google/genai";
import { z } from "zod";
const Recipe = z.object({
name: z.string().describe("Name of the dish"),
ingredients: z.array(z.string()),
prepMinutes: z.number().int().nonnegative(),
difficulty: z.enum(["easy", "medium", "hard"]),
});
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const resp = await ai.models.generateContent({
model: "gemini-3.5-flash",
contents: "Recipe for chocolate chip cookies.",
config: {
responseMimeType: "application/json",
responseSchema: Recipe,
},
});
const recipe = JSON.parse(resp.text); // or resp.parsed5. Enum mode
For single-value classification, MIME type text/x.enum returns just the enum value, not a JSON object:
config=types.GenerateContentConfig(
response_mime_type="text/x.enum",
response_schema={
"type": "STRING",
"enum": ["positive", "neutral", "negative"],
},
)
resp = client.models.generate_content(
model="gemini-3.5-flash",
contents="Classify sentiment: 'I love this product.'",
config=config,
)
print(resp.text) # "positive" (bare value, no JSON wrapping)With Pydantic:
from enum import Enum
class Sentiment(Enum):
POSITIVE = "positive"
NEUTRAL = "neutral"
NEGATIVE = "negative"
config=types.GenerateContentConfig(
response_mime_type="text/x.enum",
response_schema=Sentiment,
)6. Supported schema features
Per ai.google.dev/gemini-api/docs/structured-output:
| Feature | Support |
|---|---|
type (STRING, NUMBER, INTEGER, BOOLEAN, OBJECT, ARRAY) | Yes |
nullable | Yes |
enum (on STRING, NUMBER, INTEGER) | Yes — constrained-decoded |
format (date, date-time, time on strings) | Yes |
description / title | Yes — strongly influence output |
properties, required, additionalProperties (limited) | Yes |
items (for arrays), minItems, maxItems | Yes |
minimum, maximum (numbers) | Yes |
propertyOrdering | Yes (Gemini 2.0 + recommended; 2.5+ honors implicitly but explicit helps) |
$ref / definitions | No |
allOf, oneOf, anyOf | No |
not, if/then/else | No |
| Recursive references | No |
pattern (regex) | Partial |
propertyOrdering matters because the model’s sampling follows the order — listing the more “anchoring” fields first (the ones whose value constrains later fields) improves output quality. Always include it on Gemini 2.0; recommended on 2.5+ and 3.x.
7. Multi-field extraction example
class Receipt(BaseModel):
merchant: str
date: str # ISO date
line_items: list[dict] = Field(description="List of {description, amount}")
subtotal: float
tax: float
total: float
resp = client.models.generate_content(
model="gemini-3.5-flash",
contents=[
"Extract receipt data.",
types.Part.from_bytes(data=receipt_image_bytes, mime_type="image/jpeg"),
],
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=Receipt,
),
)
receipt: Receipt = resp.parsedPair with vision input (above) or PDF input for OCR-then-structure pipelines.
8. Reliability guarantees
Per the docs, JSON mode + schema provides:
- Syntactic guarantee: the response will be valid JSON that parses against the schema
- NOT a semantic guarantee: the model can still hallucinate values, miss data, or be wrong about content
Always validate output semantically after parsing. Pydantic helps with type coercion + @validator decorators.
9. JSON mode without schema
config=types.GenerateContentConfig(
response_mime_type="application/json",
# no response_schema
)The model returns valid JSON but the shape is its choice. Useful for exploratory extraction. Combine with system_instruction to nudge shape:
system_instruction="Always respond with JSON of the form {\"answer\": <str>, \"confidence\": <0-1>}"10. Mixing with thinking
JSON mode + thinking works. The thinking phase runs first (reasoning about the right values to populate), then the JSON output is constrained-decoded:
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=ComplexSchema,
thinking_config=types.ThinkingConfig(thinking_budget=2048),
)Thinking is particularly helpful on schemas with description fields that demand reasoning (e.g. “classify based on subtle criteria”). Budget cost: thinking tokens are billed at output rate.
11. Mixing with tools
You cannot mix response_schema with tools in the same request — they’re alternative paths. If you need both structured output and tool calls, structure the tool’s args/result with their own schemas instead.
Workaround pattern: use tools to gather data, then make a final generate_content call with response_schema to format.
# Pass 1: gather with tools
result = run_agent(query, tools=[search, fetch])
# Pass 2: structure
final = client.models.generate_content(
model="gemini-3.5-flash",
contents=f"Structure this output: {result}",
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=ReportSchema,
),
)12. Common pitfalls
- Schema too complex — Union types, recursive refs, deeply nested optionals → schema rejection or
MALFORMED_*finish reason. Flatten. - Pydantic v1 vs v2 — the SDK targets Pydantic v2 (
from pydantic import BaseModel). v1 syntax may not serialize correctly to the schema. Optional[X]semantics —Optional[str]translates tonullable: true. If you also want it absent (not just null), setdefault=Noneand exclude fromrequired.- Enum collision with
additionalProperties— combiningadditionalProperties: falsewith optional enum fields can cause refusals. Test withcount_tokensto surface schema errors. - Forgetting
response_mime_type— setting onlyresponse_schemawithoutresponse_mime_type="application/json"is a no-op (silently ignored on some SDK paths). - Default fields shifting — Pydantic
Field(default=...)adds adefaultto the schema, which Gemini may interpret as a hint to use that value. To force the model to fill, useField(...)without default and put it inrequired. - Token cost — large schemas (50+ fields) add significant input tokens. Cache the schema (in system instruction or explicit cache) when reusing.
- JSON keys with hyphens — JSON keys with hyphens may not round-trip cleanly. Use snake_case in field names and rename for downstream consumers.
13. Comparison to other vendors
| Aspect | Gemini | Claude | OpenAI |
|---|---|---|---|
| API surface | response_mime_type + response_schema | tools[].input_schema + tool_choice: tool | response_format: {type: "json_schema", json_schema: ...} |
| Strict / constrained decoding | Yes (constrained sampling) | strict_input: true (tool args only) | strict: true (full enforcement) |
| Pydantic auto-serialization | Yes (Python SDK) | Via instructor library | Yes (.parse() method on SDK) |
| Schema features supported | OpenAPI subset (no $ref / unions) | JSON Schema subset (no $ref) | JSON Schema strict (no oneOf / allOf, no recursion, all required) |
| Enum classification mode | text/x.enum (bare value) | Tool with enum field | response_format: json_schema with enum |
14. Further reading
- Structured output — canonical reference
- google-genai Pydantic integration — SDK source for serialization details
- Constrained decoding overview — the underlying technique