From 35674b3ca535d44e21af95a7d74af4e81a1ad2f1 Mon Sep 17 00:00:00 2001 From: Feldt <204218226+Feldt@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:38:15 -0400 Subject: [PATCH] fix(tools): declare repeated Google API query parameters as arrays GoogleApiToOpenApiConverter copied type, enum, format, default and pattern from each discovery parameter but ignored `repeated: true`. Every repeated query parameter (Sheets values.batchGet `ranges`, Gmail messages.list `labelIds`, Calendar events.list `eventTypes`, YouTube videos.list `id`) was therefore declared to the model as a single scalar, so the model could ask for only one value. Wrap the schema of a repeated parameter in an array of that schema, and turn its default into a one-item list. RestApiTool already sends a list query value as repeated keys (`?ranges=a&ranges=b`), so only the declaration changes. --- .../googleapi_to_openapi_converter.py | 8 +++ .../test_googleapi_to_openapi_converter.py | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py b/src/google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py index 7fad7cba9f4..b9c7df12bd9 100644 --- a/src/google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py +++ b/src/google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py @@ -527,6 +527,14 @@ def _convert_parameter_schema( if "pattern" in param_data: schema["pattern"] = param_data["pattern"] + # A repeated parameter takes a list of values, each matching the schema + # above. Its default moves to the array as a one-item list. + if param_data.get("repeated", False): + item_schema = schema + schema = {"type": "array", "items": item_schema} + if "default" in item_schema: + schema["default"] = [item_schema.pop("default")] + return schema def save_openapi_spec(self, output_path: str) -> None: diff --git a/tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py b/tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py index b975cc09924..246024d05bc 100644 --- a/tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py +++ b/tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py @@ -182,6 +182,17 @@ def calendar_api_spec(): "enum": ["startTime", "updated"], "location": "query", }, + "eventTypes": { + "type": "string", + "description": "Event types to return", + "enum": [ + "default", + "focusTime", + "outOfOffice", + ], + "repeated": True, + "location": "query", + }, }, "response": {"$ref": "Events"}, "scopes": [ @@ -581,6 +592,32 @@ def test_extract_path_parameters(self, converter, path, expected_params): {"type": "string", "enum": ["option1", "option2"]}, {"type": "string", "enum": ["option1", "option2"]}, ), + # Repeated parameter + ( + {"type": "string", "repeated": True}, + {"type": "array", "items": {"type": "string"}}, + ), + # Repeated enum parameter + ( + { + "type": "string", + "enum": ["option1", "option2"], + "repeated": True, + }, + { + "type": "array", + "items": {"type": "string", "enum": ["option1", "option2"]}, + }, + ), + # Repeated parameter with default + ( + {"type": "string", "default": "LIVE", "repeated": True}, + { + "type": "array", + "items": {"type": "string"}, + "default": ["LIVE"], + }, + ), ], ) def test_convert_parameter_schema( @@ -739,6 +776,24 @@ def test_integration_calendar_api(self, converter_with_patched_build): assert max_results["schema"]["type"] == "integer" assert max_results["schema"]["default"] == "250" + def test_convert_repeated_query_parameter_to_array( + self, converter_with_patched_build + ): + """A query parameter marked repeated accepts a list of values.""" + openapi_spec = converter_with_patched_build.convert() + + get_events = openapi_spec["paths"]["/calendars/{calendarId}/events"]["get"] + param_dict = {p["name"]: p for p in get_events["parameters"]} + event_types = param_dict["eventTypes"] + assert event_types["in"] == "query" + assert event_types["schema"] == { + "type": "array", + "items": { + "type": "string", + "enum": ["default", "focusTime", "outOfOffice"], + }, + } + @pytest.fixture def conftest_content():