Skip to content

FunctionTool does not convert Union[Pydantic, Pydantic] at runtime (visible in ADK 2.0 GA) #5799

Description

@kkj333

🔴 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:

  1. Install ADK 2.0.x (pip show google-adk)
  2. Run the minimal repro below (no API key needed for the tool execution path)
  3. Observe status: error for 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:

importasynciofromtypingimportOptional, Unionfromunittest.mockimportMagicMockimportpydanticfromgoogle.adk.tools.function_toolimportFunctionToolfromgoogle.adk.tools.tool_contextimportToolContextfromgoogle.adk.agents.invocation_contextimportInvocationContextfromgoogle.adk.sessions.sessionimportSessionclassUserProfile(pydantic.BaseModel):
name: strage: intemail: Optional[str] =NoneclassCompanyProfile(pydantic.BaseModel):
company_name: strindustry: stremployee_count: intdefcreate_entity_profile(entity: Union[UserProfile, CompanyProfile]) ->dict:
ifisinstance(entity, UserProfile):
return {"status": "user_profile_created", "entity_type": "user"}
elifisinstance(entity, CompanyProfile):
return {"status": "company_profile_created", "entity_type": "company"}
return {"status": "error", "message": f"Unexpected entity type: {type(entity)}"}
asyncdefmain():
tool=FunctionTool(create_entity_profile)
ctx=MagicMock(spec=ToolContext)
inv=MagicMock(spec=InvocationContext)
inv.session=MagicMock(spec=Session)
ctx.invocation_context=invforargsin [
{"entity": {"name": "Diana", "age": 32, "email": "d@example.com"}},
{"entity": {"company_name": "Acme Corp", "industry": "tech", "employee_count": 50}},
]:
print(awaittool.run_async(args=args, tool_context=ctx))
asyncio.run(main())

Additional Context:

How often has this issue occurred?:

  • Always (100%) for Union[BaseModel, BaseModel] tool parameters

Metadata

Metadata

Assignees

Labels

tools[Component] This issue is related to tools

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions