Skip to content

fix(tools): handle Union[ModelA, ModelB] in FunctionTool._preprocess_args#5813

Open
devteamaegis wants to merge 2 commits into
google:mainfrom
devteamaegis:fix/function-tool-union-pydantic-conversion
Open

fix(tools): handle Union[ModelA, ModelB] in FunctionTool._preprocess_args#5813
devteamaegis wants to merge 2 commits into
google:mainfrom
devteamaegis:fix/function-tool-union-pydantic-conversion

Conversation

@devteamaegis
Copy link
Copy Markdown

Problem

FunctionTool._preprocess_args silently skips dict-to-Pydantic conversion when a
parameter is typed as Union[ModelA, ModelB] (two or more non-None members).

The existing code only handled Optional[T] — i.e. exactly one non-None
type in the union:

non_none_types = [arg for arg in union_args if arg is not type(None)]
if len(non_none_types) == 1:
    target_type = non_none_types[0]          # ✅ Optional[T] works
# len > 1 → target_type stays as raw Union → issubclass check always False → dict
# passed to the function unchanged → isinstance() in tool body raises TypeError

Reported in #5799. Real-world failure:

def create_entity(entity: Union[UserProfile, CompanyProfile]) -> str:
    if isinstance(entity, UserProfile):      # ❌ entity is still a dict
        ...

Fix

Add an elif len(non_none_types) > 1 branch that, when the value is a dict,
iterates the Pydantic members of the union in declaration order and calls
model_validate() on the first one that accepts the payload — identical strategy
to the existing Optional[T] path.

elif len(non_none_types) > 1 and isinstance(args[param_name], dict):
    if not any(isinstance(args[param_name], t) for t in non_none_types
               if inspect.isclass(t)):
        pydantic_types = [
            t for t in non_none_types
            if inspect.isclass(t) and issubclass(t, pydantic.BaseModel)
        ]
        for candidate_type in pydantic_types:
            try:
                converted_args[param_name] = candidate_type.model_validate(
                    args[param_name]
                )
                break
            except Exception:
                continue
        else:
            logger.warning(...)
    continue

Tests

Five new cases added to tests/unittests/tools/test_function_tool_pydantic.py:

Test What it checks
test_preprocess_args_union_first_branch_converted dict → first union member
test_preprocess_args_union_second_branch_converted dict → second union member
test_preprocess_args_union_already_correct_type_unchanged existing instance untouched
test_preprocess_args_union_optional_backward_compat Optional[T] still works
test_run_async_union_end_to_end both branches via run_async

Fixes #5799

…args

When a tool parameter is typed as Union[PydanticA, PydanticB] the old code only
handled Optional[T] (exactly one non-None union member). With two or more concrete
types, target_type was left as the raw Union, which is not a class, so the
issubclass(target_type, pydantic.BaseModel) guard was never entered and the dict
was passed through unconverted — causing downstream isinstance() failures.

The fix adds an elif branch for len(non_none_types) > 1 that iterates the
Pydantic members in declaration order and calls model_validate() on the first one
that accepts the dict, mirroring the Optional[T] path.

Fixes google#5799
…ss_args

Covers the fix for issue google#5799 where dicts passed as Union[PydanticA, PydanticB]
arguments were not converted to model instances. Added five new test cases:
- dict matching first Union branch -> first model
- dict matching second Union branch -> second model
- already-correct instance passes through unchanged
- Optional[T] backward-compatibility still holds
- run_async end-to-end with both Union branches
@adk-bot adk-bot added the tools [Component] This issue is related to tools label May 22, 2026
@adk-bot
Copy link
Copy Markdown
Collaborator

adk-bot commented May 22, 2026

Response from ADK Triaging Agent

Hello @devteamaegis, thank you for creating this PR!

This PR is a great fix for the issue with Union types in FunctionTool. To help the reviewers merge your PR more efficiently, please ensure you've addressed the following items from our contribution guidelines:

  • Summary of passed pytest results: Please include the console output of your passed unit tests to show that they run successfully in your environment.
  • Logs or E2E evidence: Since this is a bug fix, providing some command-line logs or a brief execution log showing the fix successfully handling the Union model validation end-to-end would be highly beneficial for the reviewers.

Thank you for your valuable contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tools [Component] This issue is related to tools

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants