Skip to content

JSON Output in the Gemini API: response_mime_type and response_schema

9 min read · updated August 11, 2026

Gemini constrains output at decoding time rather than asking politely in a prompt. Two fields in generationConfig do it, and the schema they take is a specific subset of OpenAPI 3.0 with one extra field that exists nowhere else.

Two fields, both required

responseMimeType selects the output format; responseSchema constrains its shape. Setting the schema without the MIME type is the failure people hit first—the schema is ignored and you get prose.

"generationConfig": {
  "responseMimeType": "application/json",
  "responseSchema": { ... }
}

responseMimeType accepts text/plain (the default), application/json, and text/x.enum for constraining the whole response to a single value from a list—covered in enum-constrained output and in the full list of MIME type options.

When this is on, the response arrives with the JSON as the text of a normal part. There is no separate structured field:

{
  "candidates": [
    {
      "content": {
        "role": "model",
        "parts": [{ "text": "{\"severity\": \"high\", \"components\": [\"auth\"]}" }]
      },
      "finishReason": "STOP"
    }
  ]
}

So you still parse it. What the constraint buys you is that the string is JSON and matches the schema—no markdown fence around it, no “Here is the JSON you asked for:” preamble, no trailing commentary. Those three are what prompt-only JSON requests actually fail on, and they are exactly what is eliminated.

The schema subset you may use

responseSchema is not JSON Schema. It is Google’s Schema object, a subset of the OpenAPI 3.0 schema object, and the structured output documentation enumerates the supported fields. The ones that carry weight:

  • typeSTRING, NUMBER, INTEGER, BOOLEAN, ARRAY, OBJECT. Uppercase in the reference; the JSON mapping accepts lowercase.
  • enum — on a STRING, restricts it to a list of values. This is the highest-value constraint in the whole schema and the most under-used.
  • required — without it, the model may omit fields. If a field must be present, say so; a description saying “always include this” is not a constraint.
  • nullable — the honest way to let a required field carry “not applicable” rather than being silently dropped or filled with an invention.
  • description — read by the model. This is where the semantics go, and it is the difference between a schema that produces the right shape and one that produces the right content.
  • items, minItems, maxItems — array element schema and bounds.
  • format — supported for a limited set of values such as date-time and numeric widths; not a general regex facility.

What is absent matters as much. There is no $ref and no general recursion, so a self-referential tree structure cannot be expressed directly—flatten it to a list of nodes with parent ids instead. There is no pattern, so string formats you care about have to be validated after the fact. And deeply nested schemas with very many properties can be rejected outright as too complex, which is a real ceiling on generated schemas from ORM models.

propertyOrdering, which has no equivalent elsewhere

JSON objects are unordered by specification, so most structured-output APIs say nothing about field order. Gemini exposes propertyOrdering, an array of property names fixing the order in which fields are generated, and it is not a formatting nicety.

The model generates the object one token at a time, left to right. A field generated early is context for the fields generated after it. So if the object contains both a conclusion and the reasoning behind it, the order decides whether the conclusion was reached before the reasoning existed or after. Put reasoning before severity and the severity is chosen with the reasoning in context; put it after and the reasoning is a post-hoc justification of a value already committed to.

The second, duller reason to set it: without it, ordering can vary between responses, which makes exact-match comparison of two outputs and any caching keyed on the response string unreliable.

A full schema and its output

A realistic one—classifying an inbound support ticket:

{
  "contents": [
    { "role": "user", "parts": [{ "text": "Ticket: Since the Tuesday release nobody on the finance team can log in with SSO. Tried three browsers. This is blocking month-end close." }] }
  ],
  "generationConfig": {
    "responseMimeType": "application/json",
    "responseSchema": {
      "type": "OBJECT",
      "propertyOrdering": ["reasoning", "category", "severity", "components", "regression", "customer_blocked"],
      "properties": {
        "reasoning": {
          "type": "STRING",
          "description": "One or two sentences of justification, written before the fields below are decided."
        },
        "category": {
          "type": "STRING",
          "enum": ["bug", "outage", "feature_request", "question", "billing"],
          "description": "The single best-fitting category."
        },
        "severity": {
          "type": "STRING",
          "enum": ["low", "medium", "high", "critical"]
        },
        "components": {
          "type": "ARRAY",
          "minItems": 1,
          "maxItems": 3,
          "items": { "type": "STRING", "enum": ["auth", "billing", "reporting", "api", "ui", "unknown"] }
        },
        "regression": {
          "type": "BOOLEAN",
          "description": "True only if the ticket states the behaviour previously worked."
        },
        "customer_blocked": {
          "type": "BOOLEAN",
          "description": "True if the customer cannot complete a business-critical task."
        }
      },
      "required": ["reasoning", "category", "severity", "components", "regression", "customer_blocked"]
    }
  }
}

The text part of the response is then guaranteed to parse and to have those six keys with values drawn from those enums:

{
  "reasoning": "The reporter states login worked before Tuesday's release and now fails across browsers, which points to a server-side authentication regression rather than a client issue. Month-end close is blocked.",
  "category": "bug",
  "severity": "critical",
  "components": ["auth"],
  "regression": true,
  "customer_blocked": true
}

Every enum in that schema is a class of downstream bug that cannot happen: no "Critical" with a capital C, no "authentication" where your router expects "auth", no fourth component sneaking past a maxItems your database column cannot hold. That is the whole argument for writing the enums out rather than describing them in prose.

Generating the schema from a type

Writing that schema by hand and then writing a matching type in your application is two definitions of one thing, and they drift. The Google Gen AI SDKs accept a type directly and generate the schema from it:

from enum import Enum
from pydantic import BaseModel
from google import genai
from google.genai import types

class Category(str, Enum):
    BUG = "bug"
    OUTAGE = "outage"
    FEATURE_REQUEST = "feature_request"
    QUESTION = "question"
    BILLING = "billing"

class Triage(BaseModel):
    reasoning: str
    category: Category
    severity: str
    regression: bool
    customer_blocked: bool

client = genai.Client()

response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents=ticket_text,
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_schema=Triage,
    ),
)

triage = response.parsed   # a Triage instance, already validated

response.parsed is the payoff: the SDK generates the schema, sends it, parses the returned JSON and validates it against the same model, so the object you hold is typed. One definition, no drift, and a validation error surfaces at the boundary rather than three functions later.

Three caveats, all of which follow from the generation being mechanical:

  • Field order comes from declaration order. The generated schema orders properties as the class declares them, which makes the reasoning-before-conclusion point above a matter of where you put the attribute. Declare reasoning first and it is generated first.
  • Only the supported subset survives. Validators, regular-expression constraints and default values in your type have no representation in Gemini’s schema object and are silently dropped from what is sent. They still run on parse, so a value the model was never told to avoid can fail validation after generation.
  • Descriptions are load-bearing. A generated schema from a bare class carries no descriptions, and descriptions are what the model reads to know what a field means. A field called severity with no description is guesswork; add docstrings or field descriptions and the accuracy difference is the largest available from any change to the schema.

The three ways it still fails

  • Truncation. Constrained decoding does not exempt you from the output ceiling. Hit maxOutputTokens mid-object and you get a finishReason of MAX_TOKENS and a string of invalid JSON. Always check the finish reason before parsing, and see the output ceilings per model.
  • Valid shape, wrong content. The schema guarantees severity is one of four strings. It does not guarantee it is the right one. Structured output moves the failure from a parse error to a semantic error, which is harder to detect and not automatically better.
  • Blocked or empty. A safety block returns no text at all. Your parser receives an empty string, not malformed JSON, and the two need different handling.

Grounding is the other incompatibility worth knowing before you design around this: a request combining a response schema with the Google Search tool is rejected in current versions, so grounded facts in a fixed shape is a two-call pattern—ground first, then structure the result in a second call.