Skip to content

OpenAPIToolset drops additionalProperties dictionary request bodies #8037

Description

@yang0228

Describe the Bug

An OpenAPI JSON request body described as a dictionary, for example type: object with additionalProperties: {type: string} and no named properties, produces an OpenAPIToolset tool with no body parameters. Calls silently send {}, discarding the supplied dictionary.

The declaration itself has properties: {}, so a model is not given a way to supply the dictionary. Passing it as a single body argument or as individual key/value arguments does not preserve it on current main.

Steps to Reproduce

Check out google/adk-python at 1cb926d5b8d702261ec82a26d3f1c4296e0db7d7, install the base dependencies, and run the script below with PYTHONPATH pointing to that checkout's src directory. It uses only httpx.MockTransport; no model, credentials, DNS lookup, or remote API is involved.

import asyncio
import json
import httpx
from google.adk.tools.openapi_tool.openapi_spec_parser.openapi_toolset import OpenAPIToolset

async def main():
    sent = []
    def handle(request):
        sent.append(json.loads(request.content))
        return httpx.Response(200, json={"ok": True})
    spec = {
        "openapi": "3.0.3",
        "info": {"title": "Labels", "version": "1"},
        "servers": [{"url": "https://example.invalid"}],
        "paths": {"/labels": {"post": {
            "operationId": "set_labels",
            "requestBody": {"required": True, "content": {
                "application/json": {"schema": {
                    "type": "object", "additionalProperties": {"type": "string"}
                }}
            }},
            "responses": {"200": {"description": "OK"}}
        }}}
    }
    toolset = OpenAPIToolset(
        spec_dict=spec,
        httpx_client_factory=lambda: httpx.AsyncClient(transport=httpx.MockTransport(handle)),
        allow_private_network_access=True,  # custom local transport; no DNS/network
    )
    tool = toolset.get_tool("set_labels")
    print("declaration:", tool._get_declaration().model_dump(exclude_none=True))
    payload = {"owner": "alice", "team": "red"}
    await tool.run_async(args={"body": payload}, tool_context=None)
    print("expected:", payload)
    print("sent:", sent)

asyncio.run(main())

Expected Behavior

The tool declaration exposes one dictionary body argument and the request contains {"owner":"alice","team":"red"}. Existing request bodies with named properties should retain their current flattened argument interface.

Observed Behavior

declaration parameters_json_schema.properties: {}
expected: {'owner': 'alice', 'team': 'red'}
sent: [{}]

Environment

  • ADK source: 2.14.0 at 1cb926d5b8d702261ec82a26d3f1c4296e0db7d7 (main checked 2026-11-08).
  • Python 3.12.13; macOS; httpx 0.28.1; FastAPI 0.139.2; Pydantic 2.13.4.
  • LiteLLM: no. Model: N/A; this is deterministic tool construction/request serialization.
  • Frequency: every call using this dictionary schema. Regression introduction: not bisected.

Source Trace and Proposed Contribution

OpenAPIToolset → OperationParser._process_request_body expands only schema.properties for every object body. For a dictionary that list is empty. RestApiTool._prepare_request_params then reconstructs an empty object from the absent parameters.

This is distinct from #6991/#6992: the failing schema explicitly has type: object and no composition keywords. It is also distinct from #5364's FunctionDeclaration handling and #7838's missing-schema crash; the media type here has a concrete schema.

I would like to contribute a focused fix that exposes pure dictionary bodies as one body parameter and passes their entries through, while preserving the existing interface for named-property objects. Please let me know if this is already being worked on or if a different representation is preferred. Regression coverage would include typed/free-form dictionaries, empty payloads, parameter-name collisions, and a named-property control.

Activity

  1. added theissue type on Nov 9, 2026
  2. surajksharma07 commented on Nov 9, 2026

    @surajksharma07
    Collaborator

    @yang0228 Reproduced this on v2.14.0 too: the tool gets no body arg and {} goes out.

    Until #8038 lands, a workaround that worked on my end is wrapping the dict schema in the spec before passing it to the toolset, e.g. {"allOf": [{"type": "object", "additionalProperties": {"type": "string"}}]}, so ADK exposes a single body arg and sends it as is. Could you give that a try in your actual setup, ideally with a real Gemini call too, and let me know if the dict comes through correctly?

    Since the PR changes the existing open-object behaviour, it’d be good to have that checked end to end before we push it forward for review.

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

Metadata

Metadata

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions