Ver en Español
Structured Responses with Schemas and Validation
Jun 24, 2026
Updated: Jun 25, 2026

Structured Responses with Schemas and Validation

Lesson notebook: https://github.com/seagomezar/ADK Blog Posts/blob/main/Lesson_5.ipynb

In this fifth lesson we take your agent to an "enterprise" level: we standardize the output with schemas, validate data, and keep the traceability we started with the callbacks from Lesson 4. The goal is predictable responses that are easy for other apps to consume and ready for production.

Overview

  • Design an output schema for AI news with financial context.
  • Force structured output (JSON) from the agent and validate it with Pydantic.
  • Convert the validated output into Markdown and save it as an artifact.
  • Reuse callbacks to audit sources and enrich the report.
  • End to end tests and a clean shutdown of the service.

5.1 Why structured outputs?

Free form responses are great for conversation, but hard to integrate. With schemas you:

  • Standardize the format (a clear contract between agents and services).
  • Validate data (types, ranges, required fields).
  • Automate transformations (JSON to Markdown, dashboards, APIs).
  • Reduce rework caused by ambiguous responses.

5.2 Define the output schema

We'll use Pydantic to model each news item and the full report.

from typing import List, Optional
from pydantic import BaseModel, AnyHttpUrl, conlist, Field


class NewsItem(BaseModel):
    headline: str = Field(..., min_length=8)
    company: str
    ticker: Optional[str] = None
    market_data: str  # e.g. "$123.45 (+1.23%)"
    summary: str
    sources: conlist(AnyHttpUrl, min_length=1)


class ResearchReport(BaseModel):
    title: str
    items: conlist(NewsItem, min_length=3, max_length=10)
    process_log: List[str] = []

Two important Pydantic v2 details worth keeping in mind:

  • In conlist the parameters are named min_length and max_length. The old min_items and max_items are deprecated and emit deprecation warnings.
  • Optional[str] no longer implies a default of None. The field is still required (it just accepts None), so to make ticker truly optional we write Optional[str] = None.

5.3 Make the agent "schema aware"

You have two complementary paths:

  • Guided prompting: instruct the LLM to return JSON that follows the schema.
  • Programmatic validation: parse and validate the JSON with Pydantic; if it fails, retry or degrade.

5.3.1 Native ADK option: output_schema

ADK lets you force structured output by declaring a Pydantic schema on LlmAgent.output_schema.

from google.adk.agents import LlmAgent


class ReportOut(ResearchReport):
    pass


formatter = LlmAgent(
    name="report_formatter",
    model="gemini-2.5-flash",
    instruction="Return ONLY a JSON that satisfies the schema.",
    input_schema=None,
    output_schema=ReportOut,          # forces JSON
    output_key="final_report_json",   # stores the result in session.state
)

Gemini model ids and ADK version pins change frequently. gemini-2.5-flash is the recommended line at the time of this revision, but confirm the current id and ADK version in the official docs before publishing.

Important: when output_schema is set, the agent cannot use tools. Use this agent only for the formatting and standardization stage.

5.3.2 Two agent pattern (tools to formatting)

  • Agent A (with tools): searches, adds financial context, and prepares a JSON or Markdown draft.
  • Agent B (with output_schema and no tools): receives the draft and returns valid JSON that satisfies the schema. Use output_key to leave the result in session.state for later consumption.

Instruction template (excerpt). The block below is a shape sketch, not a literal payload: values like str or str|null denote the expected type of each field, not text the model should copy verbatim.

Return ONLY valid JSON with this shape:
{
  "title": "AI Industry News Report",
  "items": [
    {
      "headline": str,
      "company": str,
      "ticker": str|null,
      "market_data": str,
      "summary": str,
      "sources": [url, ...]
    }
  ],
  "process_log": [str, ...]
}
Do not add comments or text before or after the JSON.

Runtime validation:

import json
from pydantic import ValidationError

raw = llm_response_text  # agent output (raw JSON)

try:
    data = json.loads(raw)
    report = ResearchReport.model_validate(data)
except (json.JSONDecodeError, ValidationError) as e:
    # Recovery strategy: ask the model to repair the JSON
    # or degrade to a simple Markdown template.
    raise

5.4 Integrate callbacks for traceability

Reuse the after tool callback from Lesson 4 to fill process_log with source domains and control actions. Include that log inside the validated JSON and in the final Markdown as well.

5.5 From validated JSON to Markdown

Generate a readable artifact from the schema:

def report_to_markdown(report: ResearchReport) -> str:
    parts = [f"# {report.title}", "\n## Top Headlines\n"]
    for i, item in enumerate(report.items, start=1):
        parts.append(
            f"### {i}. {item.headline}\n"
            f"- **Company:** {item.company} ({item.ticker or 'N/A'})\n"
            f"- **Market:** {item.market_data}\n"
            f"- **Summary:** {item.summary}\n"
            f"- **Sources:** {', '.join(map(str, item.sources))}\n"
        )
    if report.process_log:
        parts.append(
            "\n## Process Log\n"
            + "\n".join(f"- {e}" for e in report.process_log)
        )
    return "\n".join(parts)

Save both artifacts. We need Path from the standard library, and save_news_to_markdown is the helper we defined in Lesson 4 to write the Markdown file to disk.

from pathlib import Path

# save_news_to_markdown(...) comes from Lesson 4.

json_path = "ai_research_report.json"
md_path = "ai_research_report.md"

Path(json_path).write_text(report.model_dump_json(indent=2), encoding="utf-8")
save_news_to_markdown(md_path, report_to_markdown(report))

5.6 End to end tests

Run the app:

  • From the parent folder: adk web and select the agent.
  • Or directly: adk web --port 8000 app5.

Ask: "Prepare a report of 5 AI news items with tickers".

  • The agent returns valid JSON (with no extra text).
  • The JSON passes Pydantic validation with no errors.
  • ai_research_report.md includes headlines, tickers, market data, and process_log.

Close processes when you finish: pkill -f "adk web".

5.7 Error recovery

  • Invalid JSON: ask the LLM to "repair the JSON" with an auto fix function, or degrade to a minimal Markdown template.
  • Missing fields: assign a default of N/A or retry with a prompt that requests only the missing fields.
  • Empty sources: require the model to include at least one URL per item, or mark the item as incomplete.

Suggested exercises

  1. Extend NewsItem with a new field (for example sentiment) using Pydantic v2 validators, and confirm that invalid JSON fails validation.
  2. Implement the auto fix strategy: when model_validate raises ValidationError, ask the model again to correct only the problematic fields.
  3. Wire up the two agent pattern end to end and confirm the validated JSON is stored in session.state under output_key.

3-point summary

  1. Pydantic schemas and output_schema give you a clear, validatable output contract that's easy for other apps to consume.
  2. When output_schema is active the agent cannot use tools, so the two agent pattern (tools to formatting) is the recommended way to combine search and structured output.
  3. From the validated JSON you can derive Markdown and other artifacts, and callbacks give you end to end traceability in process_log.

Resources

This content is based on the course "Building Live Voice Agents with Google's ADK!" by DeepLearning.AI (https://learn.deeplearning.ai/courses/building live voice agents with googles adk/). This blog aims to bring ADK material to a Spanish speaking audience.

That's all, I hope this lesson is useful to you and that you can apply it to a project you have in mind. Leave me a comment if it helped, if you want to add an opinion, or if you have any questions, and remember that if you liked it you can also share it using the social links below. Good luck!

Sebastian Gomez

Sebastian Gomez

Creador de contenido principalmente acerca de tecnología.

Leave a Reply

0 Comments

Advertisements

Related Posts

Categorias