Module 6 20 min

Structured Output & Tool Calling

Making models return JSON and call functions reliably.

Free-form text is great for humans and terrible for programs. The moment you want to do something with a model's answer — save it to a database, call a function, render a UI — you need the output in a shape your code can rely on. This lesson covers the two mechanisms that make that possible: structured output and tool calling. All the code runs against your free Gemini key.

Why does this exist?

Early LLM apps parsed answers with regexes and prayed. "Please respond with JSON" worked 95% of the time — and the other 5% crashed production at 2am with a markdown code fence or a chatty preamble. Structured output exists to make the model's response a contract: the API guarantees the shape, so your code can trust it. Tool calling exists for the opposite direction: letting the model ask your code to do things it can't — look up live data, send an email, query a database.

The problem, concretely

Ask a model to extract data and you get something like:

Sure! Here's the info you asked for:

The meeting is with **Priya Sharma** on July 9th at 3pm IST
about the Q3 roadmap. Let me know if you need anything else!

Correct — and useless to a program. You wanted:

{"name": "Priya Sharma", "date": "2026-07-09", "time": "15:00", "topic": "Q3 roadmap"}

Structured output with Gemini

Gemini supports constrained JSON output natively: you pass a schema, and the API guarantees the response parses against it. The cleanest way in Python is a Pydantic model — try it in Colab:

# pip install google-genai pydantic
import os
from google import genai
from pydantic import BaseModel

class Meeting(BaseModel):
    name: str
    date: str
    time: str
    topic: str

client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="Set up a meeting with Priya Sharma on July 9th at 3pm about the Q3 roadmap.",
    config={
        "response_mime_type": "application/json",
        "response_schema": Meeting,
    },
)

meeting = response.parsed   # a real Meeting object, not a string!
print(meeting.name, meeting.date, meeting.time)

No regexes, no json.loads in a try/except, no markdown fences. response.parsed is a validated Python object. Under the hood the API constrains which tokens the model is even allowed to sample, so invalid JSON can't be produced.

Schemas are prompts too

Field names and descriptions guide the model. date: str with the description "ISO 8601 date" gets you 2026-07-09; an unnamed string field gets you "July 9th". Design schemas like you design prompts — the model reads them.

Tool calling: the model asks, your code answers

Structured output shapes what the model says. Tool calling lets the model request actions. You describe functions to the model; when a request needs one, the model responds not with prose but with a structured call — name plus arguments — that your code executes.

  1. You declare toolsSend the model a list of function signatures: names, parameters, and descriptions of what each does.
  2. User asks something
  3. Model emits a call
  4. Your code executes
  5. Model answers with the result

With the Python SDK, you can hand Gemini an actual function and let the SDK handle the round trip:

def get_weather(city: str) -> str:
    """Return the current weather for a city."""
    # In a real app this calls a weather API. Fake it for the demo:
    return f"34°C and sunny in {city}"

response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="What's the weather in Jaipur? Should I carry a jacket?",
    config={"tools": [get_weather]},
)
print(response.text)
# "It's 34°C and sunny in Jaipur — no jacket needed..."

The SDK sent your function's signature and docstring to the model, received the structured call, executed get_weather("Jaipur"), sent the result back, and returned the final grounded answer. That loop — model decides, your code acts, model continues — is the seed of everything in the Agents module.

The model never runs code

A crucial mental model: the model only ever produces a request to call a function. Your code decides whether to execute it. That boundary is where you enforce permissions, validation, and safety — never execute model-chosen arguments blindly (imagine delete_user(id=...)).

Build it yourself

In a Colab notebook with your GEMINI_API_KEY:

  1. Define a Pydantic schema for a support ticket: severity (one of "low", "medium", "high"), category, summary. Feed it three angry customer emails you invent, and check the extracted fields.
  2. Write a convert_currency(amount: float, from_code: str, to_code: str) function with hardcoded rates. Give it to Gemini as a tool and ask "How much is 5,000 rupees in dollars?".
  3. Break it on purpose: ask the weather question but pass no tools. Watch the model either refuse or guess — that's the ungrounded behavior tool calling exists to fix.

Summary

  • Free-form text breaks programs; structured output makes the model's reply a schema-guaranteed contract (response_mime_type + response_schema, response.parsed).
  • Schemas double as prompts — name and describe fields carefully.
  • Tool calling inverts the flow: the model emits a structured request, your code executes it and returns the result.
  • The model never runs anything itself — the execution boundary is where your safety checks live.
  • Model decides → code acts → model continues: remember this loop; it becomes the agent loop in Module 8.