◉GEMINI LABJP
●TTS — Gemini 3.8 Flash TTS and Flash-Lite TTS are GA as of September 22. Voice replication works on Flash only; Flash-Lite returns a 400●2.5 — Access to the 2.5 models is now limited to accounts with prior active usage (September 18). Not a deprecation: new projects should start on 3.5 Flash-Lite or 3.8 Flash●9/30 — Two days until gemini-omni-flash-preview shuts down, and gemini-2.5-flash-image follows on October 2. In practice the replacement for the latter is gemini-3.1-flash-image●AUTH — Reports continue of Gemini CLI sign-in failing only for Workspace Enterprise accounts while personal accounts work. Isolating the two is the current focus●NEW — Moving to 3.8 Flash-Lite TTS meant re-picking the voice for our guidance audio●429 — A brand-new project shows Free tier yet returns 429 with limit: 0. Here is what to check in the first hour●TTS — Gemini 3.8 Flash TTS and Flash-Lite TTS are GA as of September 22. Voice replication works on Flash only; Flash-Lite returns a 400●2.5 — Access to the 2.5 models is now limited to accounts with prior active usage (September 18). Not a deprecation: new projects should start on 3.5 Flash-Lite or 3.8 Flash●9/30 — Two days until gemini-omni-flash-preview shuts down, and gemini-2.5-flash-image follows on October 2. In practice the replacement for the latter is gemini-3.1-flash-image●AUTH — Reports continue of Gemini CLI sign-in failing only for Workspace Enterprise accounts while personal accounts work. Isolating the two is the current focus●NEW — Moving to 3.8 Flash-Lite TTS meant re-picking the voice for our guidance audio●429 — A brand-new project shows Free tier yet returns 429 with limit: 0. Here is what to check in the first hour
Articles/API / SDK
◈ API / SDK/2026-06-18Advanced

Switching Types Per Input Kind in Gemini Structured Output — Notes on anyOf Discriminated Unions

Classifying mixed input kinds through one endpoint leaves a flat schema full of nulls. Here is how I switch types per kind with an anyOf discriminated union and parse it safely with Pydantic and Zod.

gemini-api284structured-output28anyofdiscriminated-unionindie-development5

✦ Premium Article

This started with a tiny review-aggregation batch I run for my indie apps. App Store and Google Play reviews, support inquiry emails, and the occasional refund request all flowed through one classification endpoint — and the output kept drifting. Reviews need a rating, refund requests need an order_id, but because everything went through a single flat schema, most fields were optional, and Gemini's "fill whatever looks fillable" behavior would drop a fragment of review text into order_id.

The problem was not the model's intelligence. It was that the schema I handed it gave it no structural way to decide which kind an item was. Commit to the kind first, then switch fields based on that kind — in other words, express a discriminated union with anyOf — and most of that ambiguity disappears. These are my implementation notes, all the way through to parsing safely with Pydantic and Zod. I pinned the model to gemini-3.5-flash, which went GA today.

Why a single flat schema fills up with nulls

The schema I started with was just every conceivable field listed in one place.

{
  "type": "object",
  "properties": {
    "category": { "type": "string" },
    "rating": { "type": "integer" },
    "summary": { "type": "string" },
    "order_id": { "type": "string" },
    "reason": { "type": "string" },
    "urgency": { "type": "string" }
  }
}

It looks harmless, but in production it breaks down. Different kinds need different fields, yet you cannot tighten required. A review has no use for order_id; a refund request has no use for rating. Make everything optional and the model, disliking empty slots, starts filling in irrelevant fields. In my own data, roughly 15% of inputs that should have been refund requests came through with a guessed value in rating and an empty order_id.

Downstream code then fills up with branches like if category == "refund" and order_id is None to absorb the ambiguity — and every one of those branches assumes the model put the right value in category, even though category is exactly the thing you cannot trust.

The contrast between the flat shape and the discriminated union looks like this.

AspectFlat single schemaanyOf discriminated union
Required fieldsCan't express per-kind differences, so everything trends optionalrequired can be specified strictly per kind
Wrong fillsEasy to fill irrelevant fieldsFields absent from a kind don't exist structurally
Downstream branchingHand-written ifs that trust categoryDiscriminator fixes the type; exhaustiveness checks work
ValidationOnly partially effectivePydantic / Zod discriminated unions apply directly

The idea behind an anyOf discriminated union

A discriminated union gives each variant a single field (the discriminator) whose value pins the type uniquely. In OpenAPI / JSON Schema you list the variants under anyOf and make each variant's discriminator field an enum that allows exactly one value. Narrow the allowed values to one, like kind: ["app_review"], and the moment the model picks that variant the value of kind is fixed — so on your side you only need to read kind to know the type.

Gemini's responseSchema supports a subset of OpenAPI, and anyOf, enum, required, and property_ordering are all usable within practical limits. property_ordering is what does the heavy lifting here. Generation proceeds front to back, so placing the discriminator first makes the model decide the kind before filling in that kind's fields. In my tests, moving the discriminator from last to first noticeably changed how often irrelevant fields leaked in. My operating conclusion: always put the discriminator in required and at the front of property_ordering.

✦

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
✦The actual responseSchema subset that works with anyOf, plus the property_ordering trick that forces the model to commit to a discriminator first
✦Receiving and validating with Python (Pydantic discriminated union) and TypeScript (Zod discriminatedUnion), with a single-shot repair loop
✦A design that routes unknown kinds to a DLQ instead of swallowing them, and the measured token and latency effect on 3.5 Flash
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 $15 for lifetime access
View Membership →

Related Articles

◈ API / SDK2026-05-20
Evolving Gemini API Structured Output Schemas in Production — Design Notes from an Indie App
How I rebuilt the JSON contract layer for a Gemini-powered recommendation feature in a long-running indie app — Dual-Emit, Sunset protocol, and a Python compatibility checker.
◈ 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-07-13
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.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links