Repository navigation
FunctionTool does not convert Union[Pydantic, Pydantic] at runtime (visible in ADK 2.0 GA) #5799
Description
🔴 Required Information
Describe the Bug:
FunctionTool._preprocess_args converts a JSON dict to a single Pydantic BaseModel and handles Optional[BaseModel], but it does not convert Union[BaseModel, BaseModel] arguments. When the LLM calls a tool with a dict matching one branch of the union, the dict is passed through unchanged. Tool code that uses isinstance(entity, UserProfile) then fails with Unexpected entity type: <class 'dict'>.
This is especially visible in ADK 2.0 GA: JSON_SCHEMA_FOR_FUNC_DECL changed from WIP / default OFF to EXPERIMENTAL / default ON (v2 transition, commit 1622793). Tool declaration generation now succeeds for union Pydantic params, but runtime conversion is still missing — so the agent can call the tool and still fail at execution time.
Steps to Reproduce:
- Install ADK 2.0.x (
pip show google-adk) - Run the minimal repro below (no API key needed for the tool execution path)
- Observe
status: errorfor both user and company entity payloads
Expected Behavior:
entity: Union[UserProfile, CompanyProfile] should be converted from the LLM JSON dict to the matching Pydantic instance before the tool function runs (as documented in contributing/samples/tools/pydantic_argument/README.md).
Observed Behavior:
# Both return:
# {'status': 'error', 'message': "Unexpected entity type: <class 'dict'>"}With JSON_SCHEMA_FOR_FUNC_DECL disabled, create_entity_profile also fails at declaration time:
ValueError: Failed to parse the parameter entity: Union[...UserProfile, ...CompanyProfile]
Environment Details:
- ADK Library Version: 2.0.0 (also reproducible on main)
- Desktop OS: macOS
- Python Version: 3.13
Model Information:
- LiteLLM: No
- Model: gemini-2.5-pro on Vertex AI (live repro confirmed; local repro above does not require API)
🟡 Optional Information
Regression:
Not a new conversion implementation in 2.0 — Union[BaseModel, BaseModel] conversion appears never to have been implemented in FunctionTool (only Optional[T] with a single non-None type is handled in _preprocess_args).
What changed in 2.0 GA is that default-on JSON_SCHEMA_FOR_FUNC_DECL allows tool registration/schema generation to succeed, so the broken runtime path is now hit in normal use.
Minimal Reproduction Code:
import asyncio
from typing import Optional, Union
from unittest.mock import MagicMock
import pydantic
from google.adk.tools.function_tool import FunctionTool
from google.adk.tools.tool_context import ToolContext
from google.adk.agents.invocation_context import InvocationContext
from google.adk.sessions.session import Session
class UserProfile(pydantic.BaseModel):
name: str
age: int
email: Optional[str] = None
class CompanyProfile(pydantic.BaseModel):
company_name: str
industry: str
employee_count: int
def create_entity_profile(entity: Union[UserProfile, CompanyProfile]) -> dict:
if isinstance(entity, UserProfile):
return {"status": "user_profile_created", "entity_type": "user"}
elif isinstance(entity, CompanyProfile):
return {"status": "company_profile_created", "entity_type": "company"}
return {"status": "error", "message": f"Unexpected entity type: {type(entity)}"}
async def main():
tool = FunctionTool(create_entity_profile)
ctx = MagicMock(spec=ToolContext)
inv = MagicMock(spec=InvocationContext)
inv.session = MagicMock(spec=Session)
ctx.invocation_context = inv
for args in [
{"entity": {"name": "Diana", "age": 32, "email": "d@example.com"}},
{"entity": {"company_name": "Acme Corp", "industry": "tech", "employee_count": 50}},
]:
print(await tool.run_async(args=args, tool_context=ctx))
asyncio.run(main())Additional Context:
- Sample:
contributing/samples/tools/pydantic_argument/agent.py(create_entity_profile) - Recorded failure in regression test:
contributing/samples/tools/pydantic_argument/tests/test_create_company.json(added in 7fc5b0e, May 2026) create_full_user_account(singleUserProfile+Optional[UserPreferences]) works correctly- Possibly related: PR fix: validate and coerce function tool argument types #4664 (Fixes ADK doesn't enforce argument types in function calling #4612) — switching to
pydantic.TypeAdapterin_preprocess_argsappears to fix this runtime case on the PR branch, but lacks an explicitUnion[BaseModel, BaseModel]test and is not merged yet - Related: ADK doesn't enforce argument types in function calling #4612 (general argument type validation), Feature: support Pydantic Model as function param and return type #1066 (Pydantic as function param — feature request)
How often has this issue occurred?:
- Always (100%) for
Union[BaseModel, BaseModel]tool parameters
Activity
fix(tools): handle Union[ModelA, ModelB] in FunctionTool._preprocess_…
test(tools): add Union[ModelA, ModelB] regression tests for _preproce…
fix: convert Union[Pydantic, Pydantic] tool args at runtime
fix: convert Union[Pydantic, Pydantic] tool args at runtime