From b719022f159c6c576a0c537de1950099296a4cf3 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sat, 6 Jun 2026 17:47:07 +0200 Subject: [PATCH 1/2] feat(abstractions): support map-style query parameters (type: object + additionalProperties) Add a Map branch to getSanitizedValues so that plain-object dictionary query parameters are pre-processed before they reach StdUriTemplate. Without this change, a Map value passed through addQueryParameters or addQueryParameter falls through to the else-return branch unchanged. When the map contains a null value the subsequent call to StdUriTemplate.convertNativeTypes(null) throws a NullPointerException (null.getClass() is called in the fallback throw). The fix mirrors the kiota-dotnet SanitizeDictionary approach exactly: null-valued entries are dropped (RFC 6570 para 2.3 treats undefined variables as absent), and remaining values are normalised to String via the existing getSanitizedValues pipeline. The sanitized HashMap is returned so StdUriTemplate.isMap/addMapValue can expand it as individual key=value pairs for RFC 6570 {?param*} templates. The @SuppressWarnings("unchecked") annotation suppresses the unavoidable raw-type cast of Map -- Java generics are erased at runtime so the cast to the generic Map type is unverifiable by the compiler, but safe because the entries are accessed only as Object. Co-authored-by: Cursor --- .../microsoft/kiota/RequestInformation.java | 15 ++ .../kiota/RequestInformationTest.java | 150 ++++++++++++++++++ 2 files changed, 165 insertions(+) diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java index be16d49e..48348980 100644 --- a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java +++ b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java @@ -476,6 +476,21 @@ private static Object getSanitizedValues(Object value) { return result; } return Arrays.asList(values); + } else if (value instanceof Map) { + // Pre-process map/dictionary values for StdUriTemplate expansion. + // Null values are skipped: RFC 6570 §2.3 states "if the value is undefined, + // the variable expansion results in no output". Use "" to send ?key= (empty value). + @SuppressWarnings("unchecked") + final Map rawMap = (Map) value; + final HashMap sanitized = new HashMap<>(rawMap.size()); + for (final Map.Entry entry : rawMap.entrySet()) { + if (entry.getValue() != null) { + sanitized.put( + entry.getKey().toString(), + getSanitizedValues(entry.getValue()).toString()); + } + } + return sanitized; } else if (value instanceof ValuedEnum) { return ((ValuedEnum) value).getValue(); } else if (value instanceof UUID) { diff --git a/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java b/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java index d1c07d1c..719f6476 100644 --- a/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java +++ b/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java @@ -473,6 +473,149 @@ void ReplacesEnumValuesPathParameters() throws IllegalStateException, URISyntaxE assertEquals("http://localhost/1,2", uri.toString()); } + @Test + void ExpandsMapQueryParameterAsIndividualKeyValuePairs() + throws IllegalStateException, URISyntaxException { + // Arrange + final RequestInformation requestInfo = new RequestInformation(); + requestInfo.httpMethod = HttpMethod.GET; + requestInfo.urlTemplate = "http://localhost/articles{?query*}"; + + final GetQueryParameters queryParameters = new GetQueryParameters(); + queryParameters.query = new HashMap<>(); + queryParameters.query.put("filter", "equals(published,true)"); + queryParameters.query.put("sort", "-createdAt"); + + // Act + requestInfo.addQueryParameters(queryParameters); + + // Assert — RFC 6570 {?query*} expands each map entry as its own key=value pair. + // Java's URLEncoder encodes ( → %28, ) → %29, , → %2C. + final URI uri = requestInfo.getUri(); + final String uriStr = uri.toString(); + assertTrue(uriStr.contains("filter=equals%28published%2Ctrue%29")); + assertTrue(uriStr.contains("sort=-createdAt")); + } + + @Test + void SkipsNullValuesWhenExpandingMapQueryParameter() + throws IllegalStateException, URISyntaxException { + // Arrange + final RequestInformation requestInfo = new RequestInformation(); + requestInfo.httpMethod = HttpMethod.GET; + requestInfo.urlTemplate = "http://localhost/articles{?query*}"; + + final Map mapWithNull = new HashMap<>(); + mapWithNull.put("filter", "equals(published,true)"); + mapWithNull.put("sort", null); + + // Act + requestInfo.addQueryParameter("query", mapWithNull); + + // Assert — null-valued entry is silently dropped + final URI uri = requestInfo.getUri(); + final String uriStr = uri.toString(); + assertTrue(uriStr.contains("filter=equals%28published%2Ctrue%29")); + assertFalse(uriStr.contains("sort")); + } + + @Test + void DoesNotThrowWhenMapQueryParameterContainsNullValues() + throws IllegalStateException, URISyntaxException { + // Arrange + final RequestInformation requestInfo = new RequestInformation(); + requestInfo.httpMethod = HttpMethod.GET; + requestInfo.urlTemplate = "http://localhost/articles{?query*}"; + + final Map mapWithNull = new HashMap<>(); + mapWithNull.put("filter", "value"); + mapWithNull.put("sort", null); + + // Act + Assert — must not throw NullPointerException + assertDoesNotThrow(() -> requestInfo.addQueryParameter("query", mapWithNull)); + final URI uri = assertDoesNotThrow(() -> requestInfo.getUri()); + assertTrue(uri.toString().contains("filter=value")); + assertFalse(uri.toString().contains("sort")); + } + + @Test + void PreservesEmptyStringValueInMapQueryParameter() + throws IllegalStateException, URISyntaxException { + // Arrange + final RequestInformation requestInfo = new RequestInformation(); + requestInfo.httpMethod = HttpMethod.GET; + requestInfo.urlTemplate = "http://localhost/articles{?query*}"; + + final Map map = new HashMap<>(); + map.put("filter", ""); + + // Act + requestInfo.addQueryParameter("query", map); + + // Assert — empty string emits ?filter= (key present, no value) + final URI uri = requestInfo.getUri(); + assertTrue(uri.toString().contains("filter=")); + } + + @Test + void OmitsMapQueryParameterEntirelyWhenAllValuesAreNull() + throws IllegalStateException, URISyntaxException { + // Arrange + final RequestInformation requestInfo = new RequestInformation(); + requestInfo.httpMethod = HttpMethod.GET; + requestInfo.urlTemplate = "http://localhost/articles{?query*}"; + + final Map allNull = new HashMap<>(); + allNull.put("filter", null); + allNull.put("sort", null); + + // Act + requestInfo.addQueryParameter("query", allNull); + + // Assert — sanitized map is empty → StdUriTemplate omits the parameter entirely + final URI uri = requestInfo.getUri(); + assertEquals("http://localhost/articles", uri.toString()); + } + + @Test + void OmitsMapQueryParameterEntirelyWhenMapIsEmpty() + throws IllegalStateException, URISyntaxException { + // Arrange + final RequestInformation requestInfo = new RequestInformation(); + requestInfo.httpMethod = HttpMethod.GET; + requestInfo.urlTemplate = "http://localhost/articles{?query*}"; + + final GetQueryParameters queryParameters = new GetQueryParameters(); + queryParameters.query = new HashMap<>(); + + // Act + requestInfo.addQueryParameters(queryParameters); + + // Assert + final URI uri = requestInfo.getUri(); + assertEquals("http://localhost/articles", uri.toString()); + } + + @Test + void MixesMapQueryParameterWithScalarQueryParameters() + throws IllegalStateException, URISyntaxException { + // Arrange + final RequestInformation requestInfo = new RequestInformation(); + requestInfo.httpMethod = HttpMethod.GET; + requestInfo.urlTemplate = "http://localhost/articles{?%24top,query*}"; + + requestInfo.addQueryParameter("%24top", 5); + final Map map = new HashMap<>(); + map.put("filter", "active"); + requestInfo.addQueryParameter("query", map); + + // Assert + final URI uri = requestInfo.getUri(); + final String uriStr = uri.toString(); + assertTrue(uriStr.contains("%24top=5")); + assertTrue(uriStr.contains("filter=active")); + } + public final SerializationWriterFactory createMockSerializationWriterFactory( SerializationWriter writer) { final SerializationWriterFactory factoryMock = mock(SerializationWriterFactory.class); @@ -518,6 +661,12 @@ class GetQueryParameters implements QueryParameters { */ @jakarta.annotation.Nullable public PeriodAndDuration[] messageAges; + /** + * Free-form key-value query parameters (type: object + additionalProperties). + * Expanded via RFC 6570 {?query*} into individual key=value pairs. + */ + @jakarta.annotation.Nullable public Map query; + @jakarta.annotation.Nonnull public Map toQueryParameters() { final Map allQueryParams = new HashMap<>(); allQueryParams.put("%24select", select); @@ -528,6 +677,7 @@ class GetQueryParameters implements QueryParameters { allQueryParams.put("parallelDatasetIds", parallelDatasetIds); allQueryParams.put("expandChildren", expandChildren); allQueryParams.put("periods", messageAges); + allQueryParams.put("query", query); return allQueryParams; } } From de049322e556e014fc8db7659b7691fd5a3cb3e6 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Fri, 24 Jul 2026 03:00:29 +0200 Subject: [PATCH 2/2] fix(abstractions): use Map for sanitized map query parameters Address PR feedback by using HashMap instead of HashMap when sanitizing map query parameters in RequestInformation. This avoids unnecessary .toString() calls on sanitized values and preserves native object types for StdUriTemplate expansion. Also adds null key checks and tests for varied map value types. Co-authored-by: Cursor --- .../microsoft/kiota/RequestInformation.java | 8 ++-- .../kiota/RequestInformationTest.java | 40 ++++++++++++++++--- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java index 48348980..7897f9b9 100644 --- a/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java +++ b/components/abstractions/src/main/java/com/microsoft/kiota/RequestInformation.java @@ -482,12 +482,10 @@ private static Object getSanitizedValues(Object value) { // the variable expansion results in no output". Use "" to send ?key= (empty value). @SuppressWarnings("unchecked") final Map rawMap = (Map) value; - final HashMap sanitized = new HashMap<>(rawMap.size()); + final HashMap sanitized = new HashMap<>(rawMap.size()); for (final Map.Entry entry : rawMap.entrySet()) { - if (entry.getValue() != null) { - sanitized.put( - entry.getKey().toString(), - getSanitizedValues(entry.getValue()).toString()); + if (entry.getKey() != null && entry.getValue() != null) { + sanitized.put(entry.getKey().toString(), getSanitizedValues(entry.getValue())); } } return sanitized; diff --git a/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java b/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java index 719f6476..a6251509 100644 --- a/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java +++ b/components/abstractions/src/test/java/com/microsoft/kiota/RequestInformationTest.java @@ -505,7 +505,7 @@ void SkipsNullValuesWhenExpandingMapQueryParameter() requestInfo.httpMethod = HttpMethod.GET; requestInfo.urlTemplate = "http://localhost/articles{?query*}"; - final Map mapWithNull = new HashMap<>(); + final Map mapWithNull = new HashMap<>(); mapWithNull.put("filter", "equals(published,true)"); mapWithNull.put("sort", null); @@ -527,7 +527,7 @@ void DoesNotThrowWhenMapQueryParameterContainsNullValues() requestInfo.httpMethod = HttpMethod.GET; requestInfo.urlTemplate = "http://localhost/articles{?query*}"; - final Map mapWithNull = new HashMap<>(); + final Map mapWithNull = new HashMap<>(); mapWithNull.put("filter", "value"); mapWithNull.put("sort", null); @@ -546,7 +546,7 @@ void PreservesEmptyStringValueInMapQueryParameter() requestInfo.httpMethod = HttpMethod.GET; requestInfo.urlTemplate = "http://localhost/articles{?query*}"; - final Map map = new HashMap<>(); + final Map map = new HashMap<>(); map.put("filter", ""); // Act @@ -565,7 +565,7 @@ void OmitsMapQueryParameterEntirelyWhenAllValuesAreNull() requestInfo.httpMethod = HttpMethod.GET; requestInfo.urlTemplate = "http://localhost/articles{?query*}"; - final Map allNull = new HashMap<>(); + final Map allNull = new HashMap<>(); allNull.put("filter", null); allNull.put("sort", null); @@ -605,7 +605,7 @@ void MixesMapQueryParameterWithScalarQueryParameters() requestInfo.urlTemplate = "http://localhost/articles{?%24top,query*}"; requestInfo.addQueryParameter("%24top", 5); - final Map map = new HashMap<>(); + final Map map = new HashMap<>(); map.put("filter", "active"); requestInfo.addQueryParameter("query", map); @@ -616,6 +616,34 @@ void MixesMapQueryParameterWithScalarQueryParameters() assertTrue(uriStr.contains("filter=active")); } + @Test + void ExpandsMapQueryParameterWithVariedValueTypes() + throws IllegalStateException, URISyntaxException { + // Arrange + final RequestInformation requestInfo = new RequestInformation(); + requestInfo.httpMethod = HttpMethod.GET; + requestInfo.urlTemplate = "http://localhost/articles{?query*}"; + + final Map map = new HashMap<>(); + map.put("top", 10); + map.put("active", true); + map.put("enumVal", TestEnum.First); + map.put("nullVal", null); + map.put(null, "ignoredNullKey"); + + // Act + requestInfo.addQueryParameter("query", map); + + // Assert + final URI uri = requestInfo.getUri(); + final String uriStr = uri.toString(); + assertTrue(uriStr.contains("top=10")); + assertTrue(uriStr.contains("active=true")); + assertTrue(uriStr.contains("enumVal=1")); + assertFalse(uriStr.contains("nullVal")); + assertFalse(uriStr.contains("ignoredNullKey")); + } + public final SerializationWriterFactory createMockSerializationWriterFactory( SerializationWriter writer) { final SerializationWriterFactory factoryMock = mock(SerializationWriterFactory.class); @@ -665,7 +693,7 @@ class GetQueryParameters implements QueryParameters { * Free-form key-value query parameters (type: object + additionalProperties). * Expanded via RFC 6570 {?query*} into individual key=value pairs. */ - @jakarta.annotation.Nullable public Map query; + @jakarta.annotation.Nullable public Map query; @jakarta.annotation.Nonnull public Map toQueryParameters() { final Map allQueryParams = new HashMap<>();