GEMINI LABJP
PRO35 — July 17, the date reports had pointed to, has passed without an official Gemini 3.5 Pro announcement or model card. July 24 is being cited as the fallbackNB2LITE — Nano Banana 2 Lite, otherwise known as Gemini 3.1 Flash-Lite Image, arrives as the fastest of the family: roughly four seconds per image at $0.034 per thousandOMNI — Gemini Omni Flash enters public preview, generating video up to ten seconds long at $0.10 per second of outputEDIT — Omni Flash is built around conversational editing. Swap a character, relight a scene, or change the angle in plain language, and the original audio and video tracks stay intactSYNTHID — Both new models carry SynthID watermarking, so anything they produce can be checked for provenance from inside the Gemini appSHUTDOWN — The older image generation models are deprecated and switch off on August 17. Worth checking your migration windowPRO35 — July 17, the date reports had pointed to, has passed without an official Gemini 3.5 Pro announcement or model card. July 24 is being cited as the fallbackNB2LITE — Nano Banana 2 Lite, otherwise known as Gemini 3.1 Flash-Lite Image, arrives as the fastest of the family: roughly four seconds per image at $0.034 per thousandOMNI — Gemini Omni Flash enters public preview, generating video up to ten seconds long at $0.10 per second of outputEDIT — Omni Flash is built around conversational editing. Swap a character, relight a scene, or change the angle in plain language, and the original audio and video tracks stay intactSYNTHID — Both new models carry SynthID watermarking, so anything they produce can be checked for provenance from inside the Gemini appSHUTDOWN — The older image generation models are deprecated and switch off on August 17. Worth checking your migration window
Articles/API / SDK
API / SDK/2026-06-25Advanced

When Gemini's Structured Output Quietly Drifts From Your Schema — Field Notes on Measuring Validation and Retries

Even with response_schema set, Gemini's structured output occasionally drifts in production. Stop swallowing failures, measure them, split causes by finish_reason, and feed errors back for a corrected retry. Field notes from stabilizing a validation pipeline.

gemini-api276structured-output22pydanticvalidation3production139reliability7

Premium Article

You set response_schema, yet the production logs keep showing scattered ValidationErrors. You try to reproduce it locally, fire the same prompt fifty times, and every one parses cleanly. But roughly once in a few thousand requests, rating — which is supposed to be an int — comes back as the string "9 points", and the downstream aggregation falls over.

Structured output is mostly honored, not always honored. What makes it nasty is how quietly the drift happens. If it threw a loud exception you'd notice; but wrapped in a try/except, the failure gets swapped for a null or a default, and your data slowly turns murky instead.

This is how I eventually settled on running structured output in production, with code you can lift. Three ideas carry the weight: treat failures as a rate, not an exception; split the cause with finish_reason; and instead of blindly retrying, hand the error back to the model and let it fix itself. Think of it as closing the gap between a feature the docs call "supported" and something that actually survives unattended traffic.

"It runs" and "it doesn't fall over" are different claims

The happy path is simple. Pass a Pydantic v2 model as response_schema, then parse the returned JSON with model_validate_json.

import os
from pydantic import BaseModel, Field
from google import genai
from google.genai import types
 
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
 
class ReviewSummary(BaseModel):
    product_name: str = Field(description="product name")
    rating: int = Field(description="integer rating 1-5", ge=1, le=5)
    summary: str = Field(description="summary under 120 chars")
 
resp = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="Summarize this review: ...",
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_schema=ReviewSummary,
    ),
)
review = ReviewSummary.model_validate_json(resp.text)

As a demo this is flawless, and your team will rightly say "look, it comes back typed." The trouble is that it only models the happy path. The route where resp.text is None, the route where slightly-off JSON comes back, the route where a safety filter cuts it short — all of them are real in production. There's a clear distance between a demo that worked once and a job that runs tens of thousands of times unattended without breaking.

Hold failure as a rate, not an exception

The first move isn't smarter handling — it's measurement. Whether your structured-output failure rate is 0.1% or 5% completely changes what you should do about it. So I start with a thin layer that just records success and failure, nothing clever.

from dataclasses import dataclass, field
from collections import Counter
 
@dataclass
class StructuredOutputMetrics:
    total: int = 0
    success: int = 0
    failures: Counter = field(default_factory=Counter)  # by cause
 
    def record_success(self):
        self.total += 1
        self.success += 1
 
    def record_failure(self, reason: str):
        self.total += 1
        self.failures[reason] += 1
 
    @property
    def failure_rate(self) -> float:
        return 0.0 if self.total == 0 else 1 - self.success / self.total
 
    def report(self) -> str:
        top = ", ".join(f"{k}={v}" for k, v in self.failures.most_common(5))
        return f"rate={self.failure_rate:.3%} n={self.total} [{top}]"
 
METRICS = StructuredOutputMetrics()

The key is not lumping failures together — keep a per-cause Counter. "Cut off because finish_reason was MAX_TOKENS" and "JSON was complete but rating was out of range" call for entirely different fixes. Blend them into a single "3% failure" number and you'll never know where to look. I dump this report() to logs hourly and page myself when failure_rate jumps past three times its baseline — because the day a model version flips, or right after I touch the prompt, that number quietly spikes.

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
How to instrument structured-output failures as a rate, not an exception, and alert on a threshold
Branching on finish_reason vs. empty text vs. schema drift so retry policy fits the actual cause
A corrected-retry pattern that feeds the error back, plus the Union/strict/deep-nesting landmines to avoid
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-07-05
Catching only the deprecations that touch you — feeding the official changelog to url-context
I found out an image model was being shut down three days before the deadline. Here is a deprecation radar that reads the official changelog through url-context and surfaces only the models I actually use, with working Python and the over-alerting tuning I had to do in production.
API / SDK2026-06-25
Gemini API × TypeScript Type-Safe AI Application Architecture — Integrating Zod Schemas, Structured Output, and Streaming
Type-safe AI applications with the Gemini API and TypeScript: Zod validation, Structured Output, streaming pipelines, and error handling that holds up in production.
API / SDK2026-06-13
The Morning Gemini Generated Fine but the Publish Crashed — A 'Generation Outbox' So Expensive Output Is Never Lost
Generation succeeds, then the process dies right before publishing. The expensive output is gone, and you pay for the same generation again. Here is a 'generation outbox' that persists the output first and turns publishing into an idempotent follow-up, plus what it did for me during the June outage.
📚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
See all →