diff --git a/extensions/resteasy-reactive/rest-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java b/extensions/resteasy-reactive/rest-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java
index 369ae603e4610..7d7a0cd97f248 100644
--- a/extensions/resteasy-reactive/rest-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java
+++ b/extensions/resteasy-reactive/rest-servlet/runtime/src/main/java/io/quarkus/resteasy/reactive/server/servlet/runtime/ServletRequestContext.java
@@ -15,6 +15,7 @@
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
import jakarta.enterprise.event.Event;
import jakarta.servlet.AsyncContext;
@@ -24,10 +25,14 @@
import jakarta.servlet.WriteListener;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.SecurityContext;
import org.jboss.resteasy.reactive.server.core.Deployment;
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
+import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor;
+import org.jboss.resteasy.reactive.server.handlers.ParameterHandler;
import org.jboss.resteasy.reactive.server.spi.ServerHttpRequest;
import org.jboss.resteasy.reactive.server.spi.ServerHttpResponse;
import org.jboss.resteasy.reactive.server.spi.ServerRestHandler;
@@ -262,6 +267,40 @@ public List getAllQueryParams(String name) {
return context.queryParam(name);
}
+ /**
+ * Retrieves the parameters from the current HTTP request as a
+ * {@link Map>}, where the keys are parameter names
+ * and the values are lists of parameter values. This allows parameters
+ * to be extracted from the URL without knowing their names in advance.
+ *
+ * The method is used by {@link ParameterExtractor}, which works with characteristics
+ * such as parameter name, single/multiple values, and encoding. Since it's
+ * not always possible to distinguish between {@link Map} and {@link MultivaluedMap},
+ * the method returns a unified {@link Map>} for handling
+ * both cases downstream by {@link ParameterHandler}.
+ *
+ * @return a {@link Map>} containing the parameters and
+ * their corresponding values.
+ */
+ @Override
+ public Map> getParametersMap() {
+ MultiMap entries = context.request().params();
+ final MultivaluedHashMap result = new MultivaluedHashMap<>();
+ if (!entries.isEmpty()) {
+ for (Map.Entry entry : entries) {
+ result.add(entry.getKey(), entry.getValue());
+ }
+
+ }
+ Map> params = result.entrySet().stream()
+ .collect(Collectors.toMap(
+ Map.Entry::getKey,
+ Map.Entry::getValue // Values are already a List
+ ));
+
+ return params;
+ }
+
@Override
public String query() {
return request.getQueryString();
diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/MapWithParamConverterTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/MapWithParamConverterTest.java
index 419dfdd721533..f48a1db6dc0a3 100644
--- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/MapWithParamConverterTest.java
+++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/MapWithParamConverterTest.java
@@ -2,6 +2,8 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -22,6 +24,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
@@ -94,10 +97,26 @@ public T fromString(String value) {
return (T) value;
}
try {
- return genericType != null ? objectMapper.readValue(value, genericType)
- : objectMapper.readValue(value, rawType);
+ JsonNode jsonNode = objectMapper.readTree(value);
+ if (jsonNode.isArray()) {
+ // Process as a list of maps and merge them into a single map
+ JavaType listType = objectMapper.getTypeFactory()
+ .constructCollectionType(List.class, rawType);
+ List
+ *
+ * @param context The ResteasyReactiveRequestContext containing the HTTP request.
+ * @return An immutable list of all extracted query parameters.
+ */
+ public Object extractParameter(ResteasyReactiveRequestContext context) {
+ Pattern commaPattern = Pattern.compile(",");
+
+ List allQueryParams = context.serverRequest().queryParamNames().stream()
+ .map(context.serverRequest()::getAllQueryParams)
+ .filter(Objects::nonNull)
+ .flatMap(List::stream)
+ .flatMap(value -> commaPattern.splitAsStream(value))
+ .collect(Collectors.toList());
+
+ return List.copyOf(allQueryParams);
+ }
+
+}
diff --git a/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/SpringMultiValueMapParamExtractor.java b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/SpringMultiValueMapParamExtractor.java
new file mode 100644
index 0000000000000..50369a7e624d5
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/SpringMultiValueMapParamExtractor.java
@@ -0,0 +1,21 @@
+package io.quarkus.spring.web.resteasy.reactive.runtime;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
+import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+@SuppressWarnings("ForLoopReplaceableByForEach")
+public class SpringMultiValueMapParamExtractor implements ParameterExtractor {
+
+ @Override
+ public Object extractParameter(ResteasyReactiveRequestContext context) {
+ Map> parametersMap = context.serverRequest().getParametersMap();
+ MultiValueMap springMap = new LinkedMultiValueMap<>();
+ parametersMap.forEach(springMap::put);
+ return springMap;
+ }
+}
diff --git a/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/SpringRequestParamHandler.java b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/SpringRequestParamHandler.java
new file mode 100644
index 0000000000000..bf758fdec10b2
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/runtime/src/main/java/io/quarkus/spring/web/resteasy/reactive/runtime/SpringRequestParamHandler.java
@@ -0,0 +1,38 @@
+package io.quarkus.spring.web.resteasy.reactive.runtime;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.jboss.resteasy.reactive.common.jaxrs.ResponseImpl;
+import org.jboss.resteasy.reactive.common.model.ResourceClass;
+import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
+import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer;
+import org.jboss.resteasy.reactive.server.model.ServerResourceMethod;
+import org.jboss.resteasy.reactive.server.spi.ServerRestHandler;
+
+/**
+ * In Spring, parameters annotated with {@code @RequestParam} are required by default unless explicitly marked as
+ * optional.
+ * This {@link SpringRequestParamHandler} enforces the required constraint responding with a BAD_REQUEST status.
+ *
+ */
+public class SpringRequestParamHandler implements HandlerChainCustomizer {
+ @Override
+ public List handlers(HandlerChainCustomizer.Phase phase, ResourceClass resourceClass,
+ ServerResourceMethod resourceMethod) {
+ if (phase == Phase.AFTER_RESPONSE_CREATED) {
+ return Collections.singletonList(new ServerRestHandler() {
+ @Override
+ public void handle(ResteasyReactiveRequestContext requestContext) throws Exception {
+ Map> parametersMap = requestContext.serverRequest().getParametersMap();
+ if (parametersMap.isEmpty()) {
+ ResponseImpl response = (ResponseImpl) requestContext.getResponse().get();
+ response.setStatus(400);
+ }
+ }
+ });
+ }
+ return Collections.emptyList();
+ }
+}
diff --git a/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/requestparam/RequestParamController.java b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/requestparam/RequestParamController.java
new file mode 100644
index 0000000000000..b5b083ee1c267
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/requestparam/RequestParamController.java
@@ -0,0 +1,72 @@
+package io.quarkus.spring.web.requestparam;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping
+public class RequestParamController {
+
+ @GetMapping("/api/foos")
+ @ResponseBody
+ public String getFoos(@RequestParam String id) {
+ return "ID: " + id;
+ }
+
+ @PostMapping("/api/foos")
+ @ResponseBody
+ public String addFoo(@RequestParam(name = "id") String fooId, @RequestParam String name) {
+ return "ID: " + fooId + " Name: " + name;
+ }
+
+ @GetMapping("/api/foos/notParamRequired")
+ @ResponseBody
+ public String getFoosNotParamRequired2(@RequestParam(required = false) String id) {
+ return "ID: " + id;
+ }
+
+ @GetMapping("/api/foos/optional")
+ @ResponseBody
+ public String getFoosOptional(@RequestParam Optional id) {
+ return "ID: " + id.orElseGet(() -> "not provided");
+ }
+
+ @GetMapping("/api/foos/defaultValue")
+ @ResponseBody
+ public String getFoosDefaultValue(@RequestParam(defaultValue = "test") String id) {
+ return "ID: " + id;
+ }
+
+ @PostMapping("/api/foos/map")
+ @ResponseBody
+ public String updateFoos(@RequestParam Map allParams) {
+ return "Parameters are " + allParams.entrySet();
+ }
+
+ @GetMapping("/api/foos/multivalue")
+ @ResponseBody
+ public String getFoosMultiValue(@RequestParam List id) {
+ return "IDs are " + id;
+ }
+
+ @PostMapping("/api/foos/multiMap")
+ @ResponseBody
+ public String updateFoos(@RequestParam MultiValueMap allParams) {
+ String result = "";
+ for (Map.Entry> entry : allParams.entrySet()) {
+ result = "Parameters are " + entry.getKey() + "=" + entry.getValue().stream().collect(Collectors.joining(", "));
+ }
+ return result;
+ }
+
+}
diff --git a/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/requestparam/RequestParamControllerTest.java b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/requestparam/RequestParamControllerTest.java
new file mode 100644
index 0000000000000..1612024ec3174
--- /dev/null
+++ b/extensions/spring-web/resteasy-reactive/tests/src/test/java/io/quarkus/spring/web/requestparam/RequestParamControllerTest.java
@@ -0,0 +1,126 @@
+package io.quarkus.spring.web.requestparam;
+
+import static io.restassured.RestAssured.when;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+
+public class RequestParamControllerTest {
+
+ @RegisterExtension
+ static QuarkusUnitTest runner = new QuarkusUnitTest()
+ .withApplicationRoot((jar) -> jar
+ .addClasses(RequestParamController.class));
+
+ @Test
+ public void testSimpleMapping() throws Exception {
+ when().get("/api/foos?id=abc")
+ .then()
+ .statusCode(200)
+ .body(is("ID: abc"));
+
+ // In Spring, method parameters annotated with @RequestParam are required by default.
+ when().get("/api/foos")
+ .then()
+ .statusCode(400);
+ }
+
+ @Test
+ public void testSimpleMappingSpecifyingName() throws Exception {
+ when().post("/api/foos?id=abc&name=bar")
+ .then()
+ .statusCode(200)
+ .body(is("ID: abc Name: bar"));
+
+ when().post("/api/foos")
+ .then()
+ .statusCode(400);
+
+ }
+
+ @Test
+ public void testNotRequiredParam() throws Exception {
+ when().get("/api/foos/notParamRequired?id=abc")
+ .then()
+ .statusCode(200)
+ .body(is("ID: abc"));
+
+ when().get("/api/foos/notParamRequired")
+ .then()
+ .statusCode(200)
+ .body(is("ID: null"));
+ }
+
+ @Test
+ public void testOptionalParam() throws Exception {
+ when().get("/api/foos/optional?id=abc")
+ .then()
+ .statusCode(200)
+ .body(is("ID: abc"));
+
+ when().get("/api/foos/optional")
+ .then()
+ .statusCode(200)
+ .body(is("ID: not provided"));
+ }
+
+ @Test
+ public void testDefaultValueForParam() throws Exception {
+ when().get("/api/foos/defaultValue?id=abc")
+ .then()
+ .statusCode(200)
+ .body(is("ID: abc"));
+
+ when().get("/api/foos/defaultValue")
+ .then()
+ .statusCode(200)
+ .body(is("ID: test"));
+ }
+
+ @Test
+ public void testMultipleMapping() throws Exception {
+ when().post("/api/foos/map?id=abc&name=bar")
+ .then()
+ .statusCode(200)
+ .body(containsString("Parameters are [name=[bar], id=[abc]]"));
+
+ when().post("/api/foos/map")
+ .then()
+ .statusCode(400);
+
+ }
+
+ @Test
+ public void testMultivalue() throws Exception {
+ when().get("/api/foos/multivalue?id=1,2,3")
+ .then()
+ .statusCode(200)
+ .body(containsString("IDs are [1, 2, 3]"));
+
+ when().get("/api/foos/multivalue?id=1,2,3&id=foo")
+ .then()
+ .statusCode(200)
+ .body(containsString("IDs are [1, 2, 3, foo]"));
+
+ when().get("/api/foos/multivalue")
+ .then()
+ .statusCode(400);
+ }
+
+ @Test
+ public void testMultiMap() throws Exception {
+ when().post("/api/foos/multiMap?id=abc&id=123")
+ .then()
+ .statusCode(200)
+ .body(containsString("Parameters are id=abc, 123"));
+
+ when().post("/api/foos/multiMap")
+ .then()
+ .statusCode(400);
+ }
+
+}
diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java
index 102499b55c060..869c074e2adae 100644
--- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java
+++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/EndpointIndexer.java
@@ -31,10 +31,12 @@
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.LOCAL_DATE_TIME;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.LOCAL_TIME;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.LONG;
+import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MAP;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MATRIX_PARAM;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI_PART_DATA_INPUT;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI_PART_FORM_PARAM;
+import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI_VALUED_MAP;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.NON_BLOCKING;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OBJECT;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OFFSET_DATE_TIME;
@@ -1387,6 +1389,14 @@ && isParameterContainerType(paramType.asClassType())) {
handleSortedSetParam(existingConverters, errorLocation, hasRuntimeConverters, builder, elementType,
currentMethodInfo);
}
+ } else if ((pt.name().equals(MAP) || pt.name().equals(MULTI_VALUED_MAP)) && type != ParameterType.BODY) {
+ typeHandled = true;
+ builder.setSingle(false);
+ elementType = toClassName(pt.arguments().get(0), currentClassInfo, actualEndpointInfo, index);
+ if (convertible) {
+ handleMapParam(existingConverters, errorLocation, hasRuntimeConverters, builder, elementType,
+ currentMethodInfo);
+ }
} else if (pt.name().equals(OPTIONAL)) {
typeHandled = true;
elementType = toClassName(pt.arguments().get(0), currentClassInfo, actualEndpointInfo, index);
@@ -1547,6 +1557,10 @@ protected void handleSetParam(Map existingConverters, String err
PARAM builder, String elementType, MethodInfo currentMethodInfo) {
}
+ protected void handleMapParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters,
+ PARAM builder, String elementType, MethodInfo currentMethodInfo) {
+ }
+
protected void handleListParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters,
PARAM builder, String elementType, MethodInfo currentMethodInfo) {
}
diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java
index 35566962ad19b..01eff1d76d166 100644
--- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java
+++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java
@@ -75,6 +75,7 @@
import org.jboss.resteasy.reactive.server.core.parameters.converters.LocalDateParamConverter;
import org.jboss.resteasy.reactive.server.core.parameters.converters.LocalDateTimeParamConverter;
import org.jboss.resteasy.reactive.server.core.parameters.converters.LocalTimeParamConverter;
+import org.jboss.resteasy.reactive.server.core.parameters.converters.MapConverter;
import org.jboss.resteasy.reactive.server.core.parameters.converters.NoopParameterConverter;
import org.jboss.resteasy.reactive.server.core.parameters.converters.OffsetDateTimeParamConverter;
import org.jboss.resteasy.reactive.server.core.parameters.converters.OffsetTimeParamConverter;
@@ -534,6 +535,14 @@ protected void handleSetParam(Map existingConverters, String err
builder.setConverter(new SetConverter.SetSupplier(converter));
}
+ @Override
+ protected void handleMapParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters,
+ ServerIndexedParameter builder, String elementType, MethodInfo currentMethodInfo) {
+ ParameterConverterSupplier converter = extractConverter(elementType, index,
+ existingConverters, errorLocation, hasRuntimeConverters, builder.getAnns(), currentMethodInfo);
+ builder.setConverter(new MapConverter.MapSupplier(converter));
+ }
+
@Override
protected void handleListParam(Map existingConverters, String errorLocation, boolean hasRuntimeConverters,
ServerIndexedParameter builder, String elementType, MethodInfo currentMethodInfo) {
diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java
index e72ad3b6913df..cc5acc27fc4bf 100644
--- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java
+++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java
@@ -14,6 +14,7 @@
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.Executor;
import java.util.regex.Matcher;
@@ -870,29 +871,36 @@ public Object getQueryParameter(String name, boolean single, boolean encoded, St
}
return val;
}
-
- // empty collections must not be turned to null
- List strings = serverRequest().getAllQueryParams(name).stream()
- .filter(p -> !p.isEmpty())
- .toList();
- if (encoded) {
- List newStrings = new ArrayList<>();
- for (String i : strings) {
- newStrings.add(Encode.encodeQueryParam(i));
+ List allQueryParams = serverRequest().getAllQueryParams(name);
+ if (allQueryParams != null && !allQueryParams.isEmpty()) {
+ // empty collections must not be turned to null
+ List strings = allQueryParams.stream()
+ .filter(p -> !p.isEmpty())
+ .toList();
+ if (encoded) {
+ List newStrings = new ArrayList<>();
+ for (String i : strings) {
+ newStrings.add(Encode.encodeQueryParam(i));
+ }
+ strings = newStrings;
}
- strings = newStrings;
- }
- if (separator != null) {
- List result = new ArrayList<>(strings.size());
- for (int i = 0; i < strings.size(); i++) {
- String[] parts = strings.get(i).split(separator);
- result.addAll(Arrays.asList(parts));
+ if (separator != null) {
+ List result = new ArrayList<>(strings.size());
+ for (int i = 0; i < strings.size(); i++) {
+ String[] parts = strings.get(i).split(separator);
+ result.addAll(Arrays.asList(parts));
+ }
+ return result;
+ } else {
+ return strings;
}
- return result;
- } else {
- return strings;
}
+ Map> queryParams = serverRequest().getParametersMap();
+ if (queryParams != null && !queryParams.isEmpty()) {
+ return queryParams;
+ }
+ return Collections.EMPTY_LIST;
}
@Override
diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/MapConverter.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/MapConverter.java
new file mode 100644
index 0000000000000..1b04c88c75f40
--- /dev/null
+++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/converters/MapConverter.java
@@ -0,0 +1,100 @@
+package org.jboss.resteasy.reactive.server.core.parameters.converters;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.MultivaluedMap;
+
+import org.jboss.resteasy.reactive.server.model.ParamConverterProviders;
+
+public class MapConverter implements ParameterConverter {
+
+ private final ParameterConverter delegate;
+
+ public MapConverter(ParameterConverter delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Object convert(Object parameter) {
+ if (parameter instanceof Map) {
+ Map