🔴 Required Information
Describe the Bug:
For an OpenAPI operation whose request body schema is polymorphic (oneOf, anyOf,
allOf) or has no type, RestApiTool generates a correct function declaration with a
body parameter, the model supplies that argument — and then the HTTP request is sent
with no body at all. No exception, no warning, no log line. Content-Type: application/json is still set, so the server receives a well-formed request with
Content-Length: 0.
The two halves of one contract disagree:
-
OperationParser._process_request_body names a polymorphic/untyped body parameter
'body' (operation_parser.py:198-200 in 2.8.0):
# Prefer explicit body name to avoid empty keys when schema lacks type
# information (e.g., oneOf/anyOf/allOf) while retaining legacy behavior
# for simple scalar types.
if schema.oneOf or schema.anyOf or schema.allOf:
param_name = 'body'
elif not schema.type:
param_name = 'body'
else:
param_name = ''
-
But RestApiTool._prepare_request_params only attaches a full body for a parameter
whose original_name is empty (rest_api_tool.py:453 in 2.8.0):
else: # like string
for param in parameters:
# original_name = '' indicating this param applies to the full body.
if param.param_location == "body" and not param.original_name:
body_data = (...)
A polymorphic schema has no type, so it takes this else branch — but its parameter is
named 'body', so not param.original_name is never true, body_data stays None, and
body_kwargs["json"] is never set.
Steps to Reproduce:
pip install google-adk (reproduced on 2.8.0)
- Run the minimal reproduction script below.
- Observe that the constructed request contains neither a
json nor a data body.
Expected Behavior:
The body argument supplied by the model is serialized as the JSON request body:
json body sent: {'card': '4111-1111'}
Observed Behavior:
declared params: [('body', 'body', 'body')]
function declaration json schema: {'properties': {'body': {'oneOf': [{'properties': {'card': {'type': 'string'}}, 'type': 'object'}, {'properties': {'iban': {'type': 'string'}}, 'type': 'object'}]}}, 'required': [], 'title': 'create_payment_Arguments', 'type': 'object'}
json body sent: None
data body sent: None
Against a real HTTP server, the request arrives as:
server received Content-Type: application/json
server received Content-Length: 0
server received body: b''
tool returned: {"ok": true}
The tool reports success while having silently dropped the payload.
Environment Details:
- ADK Library Version (pip show google-adk): 2.8.0
- Desktop OS: Windows 11
- Python Version (python -V): 3.12.10
Model Information:
- Are you using LiteLLM: N/A
- Which model is being used: N/A — this reproduces directly in request construction, with
no model call involved.
🟡 Optional Information
Regression:
Not exactly a regression to a working state — the failure mode changed from loud to
silent. The mismatch was introduced by commit 084c2de0 (2025-11-20, "fix: Make sure
request bodies without explicit names are named 'body'", closing #2213). Before it,
these parameters were built with original_name='', which the sender matched; the
declaration however carried an empty-named property, which is exactly what #2213
reported (Gemini rejecting it with INVALID_ARGUMENT). That commit repaired the
declaration side — touching common.py, operation_parser.py and both their tests —
but not rest_api_tool.py, whose matching condition depends on the original_name
invariant it changed. The stale comment at rest_api_tool.py:452 still documents the
old invariant.
So: before 2025-11-20 the tool failed immediately and visibly; since then it appears to
work and silently discards the payload. Present in every release since, up to and
including 2.8.0.
Logs:
N/A — there is no log output to attach, and that is central to the bug: nothing is
logged, raised, or warned. The request is built without a body and sent as if normal.
Screenshots / Video:
N/A
Additional Context:
Scope: only top-level oneOf/anyOf/allOf or untyped request bodies are affected.
Plain type: object and type: array bodies take different branches and work correctly,
as do simple scalar bodies (which still get original_name='').
This is adjacent to #6503 (required body properties dropped), which fixed the
type: object branch of the same method — but that fix was parser-side and does not
touch this path.
Note the impact depends on the target API: some will reject the empty body with a 4xx
that is confusing to debug (the model's output was correct), while others may accept it
and perform an unintended no-op or create an empty resource.
On a possible fix: accept the parser's named full-body parameter in the same branch:
if param.param_location == "body" and param.original_name in ("", "body"):
Ideally the literal would be shared between OperationParser and RestApiTool rather
than duplicated, so the two sides cannot drift again. A regression test asserting
request_params["json"] for a oneOf body would cover the gap — test_operation_parser.py
pins the parser side ("Ensures oneOf bodies result in a named parameter") but no
test_rest_api_tool.py case covers sending such a body.
Minimal Reproduction Code:
from google.adk.tools.openapi_tool.openapi_spec_parser.openapi_spec_parser import (
OpenApiSpecParser,
)
from google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool import RestApiTool
SPEC = {
"openapi": "3.0.0",
"info": {"title": "t", "version": "1"},
"servers": [{"url": "https://example.invalid"}],
"paths": {
"/pay": {
"post": {
"operationId": "create_payment",
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {
"oneOf": [
{"type": "object",
"properties": {"card": {"type": "string"}}},
{"type": "object",
"properties": {"iban": {"type": "string"}}},
]
}
}
},
},
"responses": {"200": {"description": "ok"}},
}
}
},
}
tool = RestApiTool.from_parsed_operation(OpenApiSpecParser().parse(SPEC)[0])
params = tool._operation_parser.get_parameters()
print("declared params:", [(p.py_name, p.original_name, p.param_location) for p in params])
# The argument the model supplies, per the generated declaration:
request_params = tool._prepare_request_params(params, {"body": {"card": "4111-1111"}})
print("json body sent:", request_params.get("json"))
print("data body sent:", request_params.get("data"))
Wire-level proof against a local HTTP server (optional, self-contained)
import asyncio, json, threading
from http.server import BaseHTTPRequestHandler, HTTPServer
captured = {}
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length") or 0)
captured["body"] = self.rfile.read(length)
captured["content_type"] = self.headers.get("Content-Type")
captured["content_length"] = length
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"ok": true}')
def log_message(self, *a):
pass
server = HTTPServer(("127.0.0.1", 0), Handler)
port = server.server_address[1]
threading.Thread(target=server.serve_forever, daemon=True).start()
from google.adk.tools.openapi_tool.openapi_spec_parser.openapi_spec_parser import (
OpenApiSpecParser,
)
from google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool import RestApiTool
SPEC = {
"openapi": "3.0.0",
"info": {"title": "t", "version": "1"},
"servers": [{"url": f"http://127.0.0.1:{port}"}],
"paths": {"/pay": {"post": {
"operationId": "create_payment",
"requestBody": {"required": True, "content": {"application/json": {"schema": {
"oneOf": [
{"type": "object", "properties": {"card": {"type": "string"}}},
{"type": "object", "properties": {"iban": {"type": "string"}}},
]}}}},
"responses": {"200": {"description": "ok"}},
}}},
}
tool = RestApiTool.from_parsed_operation(OpenApiSpecParser().parse(SPEC)[0])
result = asyncio.run(tool.call(args={"body": {"card": "4111-1111"}}, tool_context=None))
server.shutdown()
print("server received C-Type: ", captured.get("content_type"))
print("server received C-Length: ", captured.get("content_length"))
print("server received body: ", captured.get("body"))
print("tool returned: ", json.dumps(result))
Output:
server received C-Type: application/json
server received C-Length: 0
server received body: b''
tool returned: {"ok": true}
How often has this issue occurred?:
🔴 Required Information
Describe the Bug:
For an OpenAPI operation whose request body schema is polymorphic (
oneOf,anyOf,allOf) or has notype,RestApiToolgenerates a correct function declaration with abodyparameter, the model supplies that argument — and then the HTTP request is sentwith no body at all. No exception, no warning, no log line.
Content-Type: application/jsonis still set, so the server receives a well-formed request withContent-Length: 0.The two halves of one contract disagree:
OperationParser._process_request_bodynames a polymorphic/untyped body parameter'body'(operation_parser.py:198-200in 2.8.0):But
RestApiTool._prepare_request_paramsonly attaches a full body for a parameterwhose
original_nameis empty (rest_api_tool.py:453in 2.8.0):A polymorphic schema has no
type, so it takes thiselsebranch — but its parameter isnamed
'body', sonot param.original_nameis never true,body_datastaysNone, andbody_kwargs["json"]is never set.Steps to Reproduce:
pip install google-adk(reproduced on 2.8.0)jsonnor adatabody.Expected Behavior:
The
bodyargument supplied by the model is serialized as the JSON request body:Observed Behavior:
Against a real HTTP server, the request arrives as:
The tool reports success while having silently dropped the payload.
Environment Details:
Model Information:
no model call involved.
🟡 Optional Information
Regression:
Not exactly a regression to a working state — the failure mode changed from loud to
silent. The mismatch was introduced by commit
084c2de0(2025-11-20, "fix: Make surerequest bodies without explicit names are named 'body'", closing #2213). Before it,
these parameters were built with
original_name='', which the sender matched; thedeclaration however carried an empty-named property, which is exactly what #2213
reported (Gemini rejecting it with INVALID_ARGUMENT). That commit repaired the
declaration side — touching
common.py,operation_parser.pyand both their tests —but not
rest_api_tool.py, whose matching condition depends on theoriginal_nameinvariant it changed. The stale comment at
rest_api_tool.py:452still documents theold invariant.
So: before 2025-11-20 the tool failed immediately and visibly; since then it appears to
work and silently discards the payload. Present in every release since, up to and
including 2.8.0.
Logs:
N/A — there is no log output to attach, and that is central to the bug: nothing is
logged, raised, or warned. The request is built without a body and sent as if normal.
Screenshots / Video:
N/A
Additional Context:
Scope: only top-level
oneOf/anyOf/allOfor untyped request bodies are affected.Plain
type: objectandtype: arraybodies take different branches and work correctly,as do simple scalar bodies (which still get
original_name='').This is adjacent to #6503 (required body properties dropped), which fixed the
type: objectbranch of the same method — but that fix was parser-side and does nottouch this path.
Note the impact depends on the target API: some will reject the empty body with a 4xx
that is confusing to debug (the model's output was correct), while others may accept it
and perform an unintended no-op or create an empty resource.
On a possible fix: accept the parser's named full-body parameter in the same branch:
Ideally the literal would be shared between
OperationParserandRestApiToolratherthan duplicated, so the two sides cannot drift again. A regression test asserting
request_params["json"]for aoneOfbody would cover the gap —test_operation_parser.pypins the parser side ("Ensures oneOf bodies result in a named parameter") but no
test_rest_api_tool.pycase covers sending such a body.Minimal Reproduction Code:
Wire-level proof against a local HTTP server (optional, self-contained)
Output:
How often has this issue occurred?: