Back to Blog
This post is Part 4 of 18 in the series: Agentic Commerce with ADK 2 — Building & Shipping CartPilot

Guaranteed Structured Output with Pydantic Schemas

Eliminate JSON parsing failures in AI workflows. Enforce guaranteed structured outputs with Pydantic models in Google ADK 2.

Share Editorial
Guaranteed Structured Output with Pydantic Schemas

Guaranteed Structured Output with Pydantic Schemas

📦 Companion Code: View the complete, runnable code on GitHub: part04 in the cartpilot-adk2-series repository.

CartPilot's cart tools already return structured dicts — but those are tool outputs, feeding back into the model. The moment we need the model's own final response to be machine-parseable — an order confirmation an email service will template, a receipt an order database will insert — "the model wrote something that looks like JSON" stops being good enough. This part fixes that with output_schema.

Why "just ask for JSON" fails in production

Prompting a plain agent with "respond only in JSON" mostly works — until it doesn't: a stray markdown code fence, a trailing comma, a field the model decided to rename, an extra sentence of preamble before the {. In development you notice. In production, a downstream parser throws on request #4,281 at 2 a.m. and you're debugging a prompt, not a schema.

output_schema sidesteps this: you give the agent a Pydantic model, and ADK constrains generation so the final response is that model — not text that resembles it.

Defining the schema

python
# cartpilot_checkout/schemas.py
from pydantic import BaseModel, Field


class OrderLine(BaseModel):
    sku: str
    name: str
    quantity: int
    line_total: float


class OrderConfirmation(BaseModel):
    """This is what downstream systems (email receipts, order DB,
    analytics) actually consume, so it must be guaranteed-valid."""

    order_id: str = Field(description="A short, human-readable order reference, e.g. CP-10234.")
    lines: list[OrderLine]
    subtotal: float
    estimated_tax: float
    total: float
    shopper_message: str = Field(
        description="A short, friendly one-line confirmation message for the shopper."
    )

Field descriptions aren't just documentation — they're instructions the model reads when deciding how to populate each field. Treat them the way you treat tool docstrings.

Wiring it to an agent

python
# cartpilot_checkout/agent.py (excerpt)
from google.adk.agents import Agent
from .schemas import OrderConfirmation

checkout_agent = Agent(
    model="gemini-3.5-flash",
    name="checkout_agent",
    description="Produces a structured order confirmation from cart contents.",
    instruction="""
You will be given cart line items, a subtotal, and a tax rate context.
Compute estimated_tax as 8% of subtotal, rounded to 2 decimals.
Compute total as subtotal + estimated_tax.
Generate a short order_id like "CP-" followed by a 5-digit number.
Write a friendly one-line shopper_message confirming the order.
""",
    output_schema=OrderConfirmation,
)

One constraint worth internalizing early: an agent with output_schema set can't also call tools in that same turn. It's a terminal, typed step — you feed it what it needs (already resolved, e.g. from a prior tool call), and it produces exactly one thing: a valid instance of the schema. That's a feature, not a limitation — it's what makes the output guaranteed rather than "usually correct."

To keep this part focused on the schema itself, our checkout tool builds the OrderConfirmation directly in Python rather than delegating to checkout_agent at runtime:

python
def checkout(tool_context: ToolContext) -> dict:
    """Finalizes the order and returns a structured confirmation."""
    summary = get_cart_summary(tool_context)
    if not summary["lines"]:
        return {"status": "empty_cart"}
    subtotal = summary["subtotal"]
    tax = round(subtotal * 0.08, 2)
    confirmation = OrderConfirmation(
        order_id=f"CP-{random.randint(10000, 99999)}",
        lines=summary["lines"],
        subtotal=subtotal,
        estimated_tax=tax,
        total=round(subtotal + tax, 2),
        shopper_message="Your CartPilot order is confirmed — thanks for shopping with us!",
    )
    return {"status": "success", "confirmation": confirmation.model_dump()}

In Part 6, when CartPilot becomes a real coordinator + specialists system, checkout_agent gets wired in as an actual sub-agent the coordinator delegates to — and its output_schema guarantee is what lets the coordinator trust the result without re-validating it.

The cost angle

Structured output isn't just a correctness fix — it's a cost fix. Every parse failure in a "just ask for JSON" approach typically costs you one of:

  • A regeneration call (ask the model to try again) — full round-trip cost, again.
  • A manual repair pass (regex/string surgery on malformed JSON) — cheap in tokens, expensive in engineering time and a permanent source of silent data corruption.
  • A retry with a stricter prompt — more input tokens every single call from then on, forever, to prevent a failure mode that schema-constrained output eliminates structurally.

output_schema collapses all of that into zero extra round-trips in the common case. It's one of the cheapest reliability wins available in ADK, and we'll lean on it again for the eval framework in Part 12 — an eval suite is much easier to grade when "correct" means "validates against this schema" rather than "reads about right."

What's next

So far CartPilot is one flat agent doing search, cart, and checkout with an ever-growing tool list and instruction block. Part 5 introduces ADK 2's Workflow Runtime — the graph-based execution engine that lets us express checkout as an explicit, deterministic pipeline instead of hoping the model calls things in the right order.