Sebastian Gomez
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
conlistthe parameters are namedmin_lengthandmax_length. The oldmin_itemsandmax_itemsare deprecated and emit deprecation warnings. Optional[str]no longer implies a default ofNone. The field is still required (it just acceptsNone), so to maketickertruly optional we writeOptional[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_schemaand no tools): receives the draft and returns valid JSON that satisfies the schema. Useoutput_keyto leave the result insession.statefor 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.
raise5.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 weband 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.mdincludes headlines, tickers, market data, andprocess_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/Aor 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
- Extend
NewsItemwith a new field (for examplesentiment) using Pydantic v2 validators, and confirm that invalid JSON fails validation. - Implement the auto fix strategy: when
model_validateraisesValidationError, ask the model again to correct only the problematic fields. - Wire up the two agent pattern end to end and confirm the validated JSON is stored in
session.stateunderoutput_key.
3-point summary
- Pydantic schemas and
output_schemagive you a clear, validatable output contract that's easy for other apps to consume. - When
output_schemais active the agent cannot use tools, so the two agent pattern (tools to formatting) is the recommended way to combine search and structured output. - From the validated JSON you can derive Markdown and other artifacts, and callbacks give you end to end traceability in
process_log.
Resources
- Output schema (LlmAgent): https://google.github.io/adk docs/api reference/python/google adk.html#google.adk.agents.LlmAgent.output_schema
- Evaluation: https://google.github.io/adk docs/evaluate/
- Pydantic: https://docs.pydantic.dev/
- Previous lesson: Callbacks and guardrails for reliable agents
- Next lesson: From development to production: streaming, memory, evaluation, deployment, and observability
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
Creador de contenido principalmente acerca de tecnología.