From b623c23183a1ee25bce1db0ab47715eab05b151e Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:33:57 -0400 Subject: [PATCH] Serialize deep object query parameters (#4523) --- .../fastmcp/utilities/openapi/director.py | 12 +++--- tests/utilities/openapi/test_director.py | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/fastmcp_slim/fastmcp/utilities/openapi/director.py b/fastmcp_slim/fastmcp/utilities/openapi/director.py index 461573960..9c8a52695 100644 --- a/fastmcp_slim/fastmcp/utilities/openapi/director.py +++ b/fastmcp_slim/fastmcp/utilities/openapi/director.py @@ -313,13 +313,13 @@ class RequestDirector: if not value: continue if explode: - # form,explode=true on objects: each property becomes - # a separate query parameter. - # e.g. {"R": 100, "G": 200} → R=100&G=200 for k, v in value.items(): - serialized[_query_scalar_to_str(k)] = _query_scalar_to_str( - v - ) + # deepObject keeps the parent parameter name; + # form style emits each property as a bare key. + property_name = _query_scalar_to_str(k) + if param_info.style == "deepObject": + property_name = f"{key}[{property_name}]" + serialized[property_name] = _query_scalar_to_str(v) else: style = param_info.style or "form" delimiter = self._STYLE_DELIMITERS.get(style, ",") diff --git a/tests/utilities/openapi/test_director.py b/tests/utilities/openapi/test_director.py index 6e8fffb82..651d8c4a9 100644 --- a/tests/utilities/openapi/test_director.py +++ b/tests/utilities/openapi/test_director.py @@ -890,6 +890,45 @@ class TestQueryParameterSerialization: assert "myAttribute=true" in url assert "data=" not in url + def test_deep_object_explode_true_uses_bracket_notation(self, director): + route = HTTPRoute( + path="/items", + method="GET", + operation_id="list_items", + parameters=[ + ParameterInfo( + name="filter", + location="query", + required=True, + schema={ + "type": "object", + "properties": { + "eq": {"type": "string"}, + "display name": {"type": "string"}, + }, + }, + explode=True, + style="deepObject", + ) + ], + parameter_map={ + "filter": {"location": "query", "openapi_name": "filter"}, + }, + ) + + request = director.build( + route, + {"filter": {"eq": "foo/bar", "display name": "active & ready"}}, + "https://example.com", + ) + + assert request.url.params["filter[eq]"] == "foo/bar" + assert request.url.params["filter[display name]"] == "active & ready" + assert "filter%5Beq%5D=foo%2Fbar" in str(request.url) + assert "filter%5Bdisplay+name%5D=active+%26+ready" in str(request.url) + assert "eq" not in request.url.params + assert "display name" not in request.url.params + def test_explode_default_dict_expands_to_separate_params(self, director): """Default explode (None → true) on objects expands properties.""" route = HTTPRoute(