◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-07-13Advanced

When responseSchema Can't Do $ref: Handling Recursive Schemas in Production with responseJsonSchema

Gemini's responseSchema is an OpenAPI subset with no $ref or $defs, so it can't express shared definitions or recursion. Here's how I moved to responseJsonSchema to reuse localized fields and handle a recursive category tree in production.

gemini119gemini-api286structured-output28json-schemapython108

✦ Premium Article

The first wall I hit trying to have Gemini classify wallpaper categories was that I couldn't express a category tree in the schema at all. Nature to Mountain to Snowy Mountain: a parent holds children, which hold their own children, and the depth varies by image. When I tried to write a self-reference in responseSchema, it simply wouldn't accept $ref. So I hand-inlined three levels and gave up on anything deeper.

What dissolved that compromise was responseJsonSchema. It's a separate field from responseSchema, and it understands JSON Schema's $ref and $defs. You can define something once and reuse it, and you can declare recursion through self-reference. As an indie developer dealing with multilingual, nested structured output, that difference maps directly onto how maintainable the whole pipeline is. This piece walks through the differences, the migration, and the traps I actually stepped on.

What responseSchema can't express

responseSchema accepts a subset of the OpenAPI 3.0 Schema object. You get type, properties, items, enum, nullable, propertyOrdering, and the later-added anyOf. But it does not interpret $ref (a reference to another definition) or $defs (a place for reusable definitions), which are at the heart of JSON Schema.

That gap bites in two situations.

First, when you want to reuse the same shape in several places. In my case, a LocalizedText object representing a Japanese/English pair is needed in three spots: the category name, the description, and the tags. With responseSchema you inline it three times, the schema grows vertically, and every time you tweak the required fields you have to fix all three without missing one.

Second, recursion. A structure like a category tree, where a node contains itself as a child, can't be expressed without references. Expanding to a fixed depth is a workaround, but with variable-depth data it breaks somewhere eventually.

AspectresponseSchemaresponseJsonSchema
Schema dialectSubset of OpenAPI 3.0JSON Schema ($ref/$defs)
Reusing shared definitionsNo (inline only)Yes, via $defs + $ref
Recursion / self-referenceNoYes
propertyOrderingSupportedNo such concept
Using bothMutually exclusive (one at a time)

Define the localized object once with $defs

Start with reuse. Using google-genai (the newer Python SDK), define LocalizedText once in $defs and reference it from several places with $ref.

from google import genai
from google.genai import types
 
client = genai.Client(api_key="YOUR_API_KEY")
 
# Define LocalizedText once, reference it from multiple places
schema = {
    "type": "object",
    "$defs": {
        "LocalizedText": {
            "type": "object",
            "properties": {
                "ja": {"type": "string"},
                "en": {"type": "string"},
            },
            "required": ["ja", "en"],
        }
    },
    "properties": {
        "name": {"$ref": "#/$defs/LocalizedText"},
        "description": {"$ref": "#/$defs/LocalizedText"},
        "tags": {
            "type": "array",
            "items": {"$ref": "#/$defs/LocalizedText"},
        },
    },
    "required": ["name", "description", "tags"],
}
 
resp = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="Give a snowy mountain wallpaper a category name, description, and tags in Japanese and English.",
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_json_schema=schema,
    ),
)
print(resp.text)

The formatted output looks like this:

{
  "name": {"ja": "雪山", "en": "Snowy Mountain"},
  "description": {"ja": "雪をかぶった山の風景", "en": "A mountain landscape covered in snow"},
  "tags": [
    {"ja": "自然", "en": "Nature"},
    {"ja": "冬", "en": "Winter"}
  ]
}

There's a single definition of LocalizedText. If I later decide to require zh alongside ja/en, I edit exactly one block inside $defs. Back when I inlined it under responseSchema, I applied the same change by hand in three places. The more definitions you accumulate, the more that difference compounds.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦If you've been inlining the same localized object over and over because responseSchema can't do $ref, you'll learn to define it once in $defs and reuse it everywhere
✦You'll declare a category tree of arbitrary depth using a self-referencing $ref instead of the fixed-depth workaround, and receive it with working code
✦You'll be able to decide between responseSchema and responseJsonSchema per task, knowing the feature gaps and the traps in each
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

◈ API / SDK2026-09-04
Screen Loop Seam Clicks With Numbers Before You Hand the Audio to Gemini
An ambient loop that clicks only at the wrap point. Here is the numeric prescreen I run before sending anything to audio understanding, why an absolute threshold fails, and how AAC encoding quietly rebuilt the seam I had just repaired.
◈ API / SDK2026-08-22
Once You Pass Twenty Mediation Groups, How Do You Find the Setting That Went Missing?
As ad mediation groups multiply, missing sources and type drift accumulate quietly. Here is the split I settled on: normalize the settings into one matrix, let code confirm the gaps, and send Gemini only the cells that need judgment.
◈ API / SDK2026-05-18
Building Automatic Wallpaper Category Classification with Gemini Vision
An indie developer shares how they implemented automatic wallpaper image classification with the Gemini Vision API — including accuracy results, real pitfalls, structured-output tips, and a cost comparison with GPT-4o Vision.