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

1. Three modes

Per ai.google.dev/gemini-api/docs/structured-output:

ModeWhen 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.parsed

3. 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.parsed

5. 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:

FeatureSupport
type (STRING, NUMBER, INTEGER, BOOLEAN, OBJECT, ARRAY)Yes
nullableYes
enum (on STRING, NUMBER, INTEGER)Yes — constrained-decoded
format (date, date-time, time on strings)Yes
description / titleYes — strongly influence output
properties, required, additionalProperties (limited)Yes
items (for arrays), minItems, maxItemsYes
minimum, maximum (numbers)Yes
propertyOrderingYes (Gemini 2.0 + recommended; 2.5+ honors implicitly but explicit helps)
$ref / definitionsNo
allOf, oneOf, anyOfNo
not, if/then/elseNo
Recursive referencesNo
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.parsed

Pair 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

  1. Schema too complex — Union types, recursive refs, deeply nested optionals → schema rejection or MALFORMED_* finish reason. Flatten.
  2. Pydantic v1 vs v2 — the SDK targets Pydantic v2 (from pydantic import BaseModel). v1 syntax may not serialize correctly to the schema.
  3. Optional[X] semanticsOptional[str] translates to nullable: true. If you also want it absent (not just null), set default=None and exclude from required.
  4. Enum collision with additionalProperties — combining additionalProperties: false with optional enum fields can cause refusals. Test with count_tokens to surface schema errors.
  5. Forgetting response_mime_type — setting only response_schema without response_mime_type="application/json" is a no-op (silently ignored on some SDK paths).
  6. Default fields shifting — Pydantic Field(default=...) adds a default to the schema, which Gemini may interpret as a hint to use that value. To force the model to fill, use Field(...) without default and put it in required.
  7. Token cost — large schemas (50+ fields) add significant input tokens. Cache the schema (in system instruction or explicit cache) when reusing.
  8. 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

AspectGeminiClaudeOpenAI
API surfaceresponse_mime_type + response_schematools[].input_schema + tool_choice: toolresponse_format: {type: "json_schema", json_schema: ...}
Strict / constrained decodingYes (constrained sampling)strict_input: true (tool args only)strict: true (full enforcement)
Pydantic auto-serializationYes (Python SDK)Via instructor libraryYes (.parse() method on SDK)
Schema features supportedOpenAPI subset (no $ref / unions)JSON Schema subset (no $ref)JSON Schema strict (no oneOf / allOf, no recursion, all required)
Enum classification modetext/x.enum (bare value)Tool with enum fieldresponse_format: json_schema with enum

14. Further reading