●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Gemini API Grounding × Structured Output Guide — Turn Web Search Results into Typed JSON Data
Combine Gemini API Grounding with Google Search and Structured Output to extract real-time web information as type-safe JSON data. Practical implementation patterns included.
You want the Gemini API to fetch external information and return it as structured data. Until recently, you couldn't do both at the same time. Enabling Grounding with Google Search blocked Structured Output, and vice versa. The Gemini 3 API update finally removed this restriction.
Getting web search results back as JSON that matches your type definitions — this combination means you can now handle tasks like "fetch current exchange rates and return them as a JSON array" or "search competitor product prices and format them for a comparison table" in a single API call.
The Problem This Combination Solves
The old approach required a two-stage pipeline.
First API call: use Grounding to fetch web information, receive a text response. Second API call: feed that text into Structured Output to get JSON. Double the cost, double the latency, and a real risk of information loss when re-feeding the text response.
The unified API lets you specify Google Search in tools while defining a JSON schema via response_mime_type. The model executes the search, interprets the results, and converts everything into your specified type — all in one step.
from google import genaifrom google.genai import typesimport jsonclient = genai.Client()# Combined Grounding + Structured Output requestresponse = client.models.generate_content( model="gemini-2.5-pro", contents="Look up the current Gemini API pricing as of April 2026, organized by model", config=types.GenerateContentConfig( tools=[types.Tool(google_search=types.GoogleSearch())], response_mime_type="application/json", response_schema={ "type": "object", "properties": { "pricing_data": { "type": "array", "items": { "type": "object", "properties": { "model_name": {"type": "string"}, "input_price_per_million": {"type": "string"}, "output_price_per_million": {"type": "string"}, "context_window": {"type": "string"}, }, "required": [ "model_name", "input_price_per_million", "output_price_per_million" ] } }, "last_updated": {"type": "string"}, "source_urls": { "type": "array", "items": {"type": "string"} } }, "required": ["pricing_data", "last_updated"] } ))# Type-safe JSON returned directlydata = json.loads(response.text)for model in data["pricing_data"]: print(f"{model['model_name']}: input {model['input_price_per_million']}/1M tokens")# Verify sources via grounding metadataif response.candidates[0].grounding_metadata: for chunk in response.candidates[0].grounding_metadata.grounding_chunks: print(f" Source: {chunk.web.title} — {chunk.web.uri}")
What you get back is structured JSON derived from live Google Search results. No fields outside your schema, no missing required fields.
Understanding the Response Structure
The combined response includes your Structured Output JSON plus grounding_metadata. This metadata contains the search queries used, source URLs, and citation information.
candidate = response.candidates[0]# 1. Structured JSON textstructured_data = json.loads(candidate.content.parts[0].text)# 2. Grounding metadatametadata = candidate.grounding_metadata# Queries the model used for searchingprint("Search queries:", metadata.web_search_queries)# → ['Gemini API pricing 2026', 'Gemini 2.5 Pro API cost per token']# Source web pagesfor chunk in metadata.grounding_chunks: print(f"Source: {chunk.web.title}") print(f" URL: {chunk.web.uri}")# Citation supports (which parts of the response are backed by which sources)for support in metadata.grounding_supports: print(f"Text: {support.segment.text}") print(f" Source indices: {support.grounding_chunk_indices}") print(f" Confidence: {support.confidence_scores}")
One thing to watch out for: when combined with Structured Output, the text response is in JSON format, so grounding_supportssegment.text may point to JSON fragments rather than readable sentences. For source verification, the grounding_chunks URL list is more reliable.
✦
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
✦Combine Grounding and Structured Output in a single call to extract web search results as type-safe JSON
✦A working verification gate that quarantines records by checking grounding_metadata provenance, with threshold design
✦End-to-end production know-how: schema design, cost structure, and the gotchas that bite real extraction pipelines
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.
Here's a production-ready pattern. This fetches competitor prices from e-commerce sites on a schedule and returns structured data for database storage.
from google import genaifrom google.genai import typesimport jsonfrom datetime import datetimeclient = genai.Client()PRICE_SCHEMA = { "type": "object", "properties": { "products": { "type": "array", "items": { "type": "object", "properties": { "product_name": {"type": "string"}, "vendor": {"type": "string"}, "price": {"type": "number"}, "currency": {"type": "string"}, "in_stock": {"type": "boolean"}, "url": {"type": "string"} }, "required": ["product_name", "vendor", "price", "currency"] } }, "search_timestamp": {"type": "string"}, "confidence_note": {"type": "string"} }, "required": ["products", "search_timestamp"]}def fetch_competitor_prices(product_query: str) -> dict: """Search competitor prices in real time and return structured data.""" try: response = client.models.generate_content( model="gemini-2.5-flash", contents=( f"Find the current retail prices for the following product " f"at major online stores. Include tax-inclusive prices " f"and stock availability.\n\n" f"Product: {product_query}\n\n" f"Stores to check: Amazon, Best Buy, Walmart" ), config=types.GenerateContentConfig( tools=[types.Tool(google_search=types.GoogleSearch())], response_mime_type="application/json", response_schema=PRICE_SCHEMA, temperature=0.1 # Low temperature for factual data extraction ) ) data = json.loads(response.text) data["search_timestamp"] = datetime.now().isoformat() # Supplement source URLs from grounding metadata if response.candidates[0].grounding_metadata: source_urls = [ chunk.web.uri for chunk in response.candidates[0].grounding_metadata.grounding_chunks ] data["source_urls"] = source_urls return data except Exception as e: return { "products": [], "search_timestamp": datetime.now().isoformat(), "confidence_note": f"Search error: {str(e)}" }# Usageresult = fetch_competitor_prices("Sony WH-1000XM6 wireless headphones")print(json.dumps(result, indent=2))# Example output:# {# "products": [# {# "product_name": "Sony WH-1000XM6",# "vendor": "Amazon",# "price": 399.99,# "currency": "USD",# "in_stock": true,# "url": "https://www.amazon.com/dp/..."# }# ],# "search_timestamp": "2026-04-12T15:30:00",# "source_urls": ["https://..."]# }
The temperature of 0.1 is deliberate. For factual data extraction like pricing, suppressing creativity produces more stable results. It's not set to zero because some flexibility helps the model interpret ambiguous search results.
Schema Design Tips for Better Accuracy
With the Grounding × Structured Output combination, schema design directly impacts response quality. Unlike regular Structured Output, the model performs a dual task: extracting information from search results and fitting it into your schema.
Keep the field count minimal. Defining more than 20 fields encourages the model to fill everything, even when search results don't contain the data. It starts hallucinating values to complete the schema. Stick to under 10 fields per request. If you need more, split across multiple calls.
Be strategic with required. Web search results don't always contain every piece of information. Fields like stock status or release dates may not be available. Leave uncertain fields out of required to give the model permission to omit what it can't verify.
Add description to your schema fields. The JSON Schema description field gives the model critical context for interpreting values correctly.
schema = { "type": "object", "properties": { "price": { "type": "number", "description": "Price in USD including tax. Return 0 if not found" }, "last_confirmed": { "type": "string", "description": "Date the price was confirmed. YYYY-MM-DD format. null if unknown" } }}
The presence or absence of description makes a noticeable difference, especially for ambiguous numeric fields. Tax inclusion, currency units, date formats — spell out the context that seems obvious to humans.
Combining with URL Context
You can use URL Context alongside Google Search. This pattern reads content directly from a specific webpage and structures it as typed data.
URL Context reads the specified page while Google Search fills in supplementary details like venue addresses and transit directions. However, using both tools together increases response latency. If speed matters, consider using only one.
Can You Trust the Extracted Data? A Verification Gate That Quarantines by Provenance
So far we've reached the point of turning search results into JSON that matches your type. But just because a field was extracted doesn't mean every row is safe to write straight into your production database. Even with Grounding enabled, fields the model filled in by inference from context can sit right next to fields actually backed by search results — inside the same JSON.
The good news is that grounding_metadata carries information about which outputs are supported by which sources. With it, you can route only the rows that have provenance into production, and quarantine the unsupported rows for human review.
from google import genaifrom google.genai import typesimport jsonclient = genai.Client()MIN_SUPPORT = 1 # at least one source requiredCONF_THRESHOLD = 0.6 # lower bound on confidence (when provided)def extract_with_provenance(prompt: str, schema: dict): resp = client.models.generate_content( model="gemini-2.5-flash", contents=prompt, config=types.GenerateContentConfig( tools=[types.Tool(google_search=types.GoogleSearch())], response_mime_type="application/json", response_schema=schema, ), ) cand = resp.candidates[0] data = json.loads(cand.content.parts[0].text) meta = cand.grounding_metadata # Set of source URLs (empty means "no search actually ran") sources = [] if meta and meta.grounding_chunks: sources = [c.web.uri for c in meta.grounding_chunks if c.web] # Aggregate confidence over the supported segments of the output supports = meta.grounding_supports if (meta and meta.grounding_supports) else [] backed = len(sources) >= MIN_SUPPORT confident = True for sup in supports: scores = getattr(sup, "confidence_scores", None) or [] if scores and max(scores) < CONF_THRESHOLD: confident = False break verdict = "accepted" if (backed and confident) else "quarantined" return { "verdict": verdict, "data": data, "sources": sources, "reason": ( "ok" if verdict == "accepted" else ("no_grounding_source" if not backed else "low_confidence") ), }schema = { "type": "object", "properties": { "price": {"type": "number", "description": "Tax-included price (JPY). 0 if not found"}, "currency": {"type": "string"}, }, "required": ["price", "currency"],}result = extract_with_provenance( "Look up the current monthly price of a 1TB cloud storage plan and return JSON", schema,)if result["verdict"] == "accepted": save_to_production(result["data"], result["sources"]) # into the production tableelse: save_to_review_queue(result) # into the quarantine queue print("quarantined:", result["reason"])
The key is that this quarantines rather than rejects. If you throw away unsupported rows as exceptions, you lose the ability to later ask "why is this data missing?" Keeping them in a review queue with a reason (no_grounding_source / low_confidence) lets you distinguish afterward between "the threshold is too strict" and "no search ran at all."
confidence_scores isn't always returned. Where it isn't, checking "is there at least one source URL?" already does a lot of the work. The realistic path is to quarantine by presence of provenance first, then add a threshold once confidence scores are available.
When you run automated data collection across several sites as an indie developer, writing search-derived numbers straight into a production table has burned me more than once — later I couldn't trace the source and got stuck during root-cause analysis. Since then, "no row enters production without provenance" has been the first rule of my extraction pipelines. Inserting a single quarantine stage noticeably cut down the accidents where a wrong number quietly spreads.
Understanding the Cost Structure
This combined feature has three cost components.
Input/output token pricing — Standard Gemini API rates. Gemini 2.5 Flash runs at $0.15/million input tokens and $0.60/million output tokens (as of April 2026).
Google Search pricing — $14 per 1,000 queries. A single request typically triggers 1–3 search queries.
Structured Output overhead — The schema definition itself consumes tokens. More complex schemas cost more, but consolidating two API calls into one usually comes out ahead.
Rough per-request cost with Gemini 2.5 Flash: about $0.015–$0.03. The two-stage pipeline cost $0.02–$0.05, so the combined approach saves 40–60%. Check the latest rates on the Gemini API pricing page.
Three Production Gotchas You Won't Find in the Docs
When you start using this in production, you'll encounter behaviors the official documentation doesn't cover.
Grounding rate limits hit before token limits. The Google Search API quota is managed separately from standard generateContent RPM limits. Under heavy load, you'll hit the Grounding quota first. For batch processing, add deliberate delays between requests, or cache results in your database for reuse.
grounding_metadata sometimes comes back empty. If the model decides a search isn't necessary, it returns JSON without invoking the Grounding tool. This is by design, not a bug. Including time-specific keywords like "latest," "current," or "as of April 2026" in your prompt increases the likelihood of triggering a search.
Streaming support is limited. Using stream=True with Grounding + Structured Output delivers partial JSON chunks that are difficult to parse incrementally. Additionally, grounding_metadata is only available after the stream completes. Unless you need real-time display, non-streaming mode is more practical.
What to Try Next
To run the code examples in this article, you need the Google AI Python SDK (pip install google-genai) and a valid API key. Start by modifying the competitor price monitoring code with a product you're interested in. By changing the schema alone, you can adapt this pattern for structured news collection, job listing extraction, technology trend monitoring, and more.
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.