From 826041d2f77eb4f9e413fba9d1683613b167d59b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 5 Jun 2025 18:50:25 +0200 Subject: [PATCH] Add Kotlin body advices This commit introduces KotlinRequestBodyAdvice and KotlinResponseBodyAdvice in order to set a KType hint when relevant. Closes gh-34923 --- ...tlinSerializationHttpMessageConverter.java | 51 +++++--------- ...ializationJsonHttpMessageConverterTests.kt | 10 ++- .../WebMvcConfigurationSupport.java | 42 ++++++++--- .../annotation/KotlinRequestBodyAdvice.java | 70 +++++++++++++++++++ .../annotation/KotlinResponseBodyAdvice.java | 67 ++++++++++++++++++ .../WebMvcConfigurationSupportTests.java | 11 ++- ...tResponseBodyMethodProcessorKotlinTests.kt | 50 ++++++++++++- 7 files changed, 252 insertions(+), 49 deletions(-) create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/KotlinRequestBodyAdvice.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/KotlinResponseBodyAdvice.java diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java index 9524dae50ff..19db6a50223 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java @@ -17,27 +17,20 @@ package org.springframework.http.converter; import java.io.IOException; -import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.List; import java.util.Map; -import kotlin.reflect.KFunction; import kotlin.reflect.KType; -import kotlin.reflect.full.KCallables; -import kotlin.reflect.jvm.ReflectJvmMapping; import kotlinx.serialization.KSerializer; import kotlinx.serialization.SerialFormat; import kotlinx.serialization.SerializersKt; import org.jspecify.annotations.Nullable; -import org.springframework.core.KotlinDetector; -import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; -import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; @@ -84,12 +77,12 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter clazz) { - return serializer(ResolvableType.forClass(clazz)) != null; + return serializer(ResolvableType.forClass(clazz), null) != null; } @Override public boolean canRead(ResolvableType type, @Nullable MediaType mediaType) { - if (!ResolvableType.NONE.equals(type) && serializer(type) != null) { + if (!ResolvableType.NONE.equals(type) && serializer(type, null) != null) { return canRead(mediaType); } else { @@ -99,7 +92,7 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter clazz, @Nullable MediaType mediaType) { - if (!ResolvableType.NONE.equals(type) && serializer(type) != null) { + if (!ResolvableType.NONE.equals(type) && serializer(type, null) != null) { return canWrite(mediaType); } else { @@ -111,7 +104,7 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter hints) throws IOException, HttpMessageNotReadableException { - KSerializer serializer = serializer(type); + KSerializer serializer = serializer(type, hints); if (serializer == null) { throw new HttpMessageNotReadableException("Could not find KSerializer for " + type, inputMessage); } @@ -129,7 +122,7 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter hints) throws IOException, HttpMessageNotWritableException { ResolvableType resolvableType = (ResolvableType.NONE.equals(type) ? ResolvableType.forInstance(object) : type); - KSerializer serializer = serializer(resolvableType); + KSerializer serializer = serializer(resolvableType, hints); if (serializer == null) { throw new HttpMessageNotWritableException("Could not find KSerializer for " + resolvableType); } @@ -149,29 +142,21 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter serializer(ResolvableType resolvableType) { - if (resolvableType.getSource() instanceof MethodParameter parameter) { - Method method = parameter.getMethod(); - Assert.notNull(method, "Method must not be null"); - if (KotlinDetector.isKotlinType(method.getDeclaringClass())) { - KFunction function = ReflectJvmMapping.getKotlinFunction(method); - if (function != null) { - KType type = (parameter.getParameterIndex() == -1 ? function.getReturnType() : - KCallables.getValueParameters(function).get(parameter.getParameterIndex()).getType()); - KSerializer serializer = this.kTypeSerializerCache.get(type); - if (serializer == null) { - try { - serializer = SerializersKt.serializerOrNull(this.format.getSerializersModule(), type); - } - catch (IllegalArgumentException ignored) { - } - if (serializer != null) { - this.kTypeSerializerCache.put(type, serializer); - } - } - return serializer; + private @Nullable KSerializer serializer(ResolvableType resolvableType, @Nullable Map hints) { + if (hints != null && hints.containsKey(KType.class.getName())) { + KType type = (KType) hints.get(KType.class.getName()); + KSerializer serializer = this.kTypeSerializerCache.get(type); + if (serializer == null) { + try { + serializer = SerializersKt.serializerOrNull(this.format.getSerializersModule(), type); + } + catch (IllegalArgumentException ignored) { + } + if (serializer != null) { + this.kTypeSerializerCache.put(type, serializer); } } + return serializer; } Type type = resolvableType.getType(); KSerializer serializer = this.typeSerializerCache.get(type); diff --git a/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt b/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt index 0b9b8f01847..01b5f44d47e 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt @@ -33,8 +33,10 @@ import org.springframework.web.testfixture.http.MockHttpOutputMessage import java.lang.reflect.ParameterizedType import java.math.BigDecimal import java.nio.charset.StandardCharsets +import kotlin.reflect.KType import kotlin.reflect.javaType import kotlin.reflect.jvm.javaMethod +import kotlin.reflect.jvm.jvmName import kotlin.reflect.typeOf /** @@ -246,7 +248,10 @@ class KotlinSerializationJsonHttpMessageConverterTests { val inputMessage = MockHttpInputMessage(body.toByteArray(StandardCharsets.UTF_8)) inputMessage.headers.contentType = MediaType.APPLICATION_JSON val methodParameter = MethodParameter.forExecutable(::handleMapWithNullable::javaMethod.get()!!, 0) - val result = converter.read(ResolvableType.forMethodParameter(methodParameter), inputMessage, null) as Map + val hints = mapOf(KType::class.jvmName to typeOf>()) + + val result = converter.read(ResolvableType.forMethodParameter(methodParameter), inputMessage, + hints) as Map assertThat(result).containsExactlyEntriesOf(mapOf("value" to null)) } @@ -400,9 +405,10 @@ class KotlinSerializationJsonHttpMessageConverterTests { val serializableBean = mapOf("value" to null) val expectedJson = """{"value":null}""" val methodParameter = MethodParameter.forExecutable(::handleMapWithNullable::javaMethod.get()!!, -1) + val hints = mapOf(KType::class.jvmName to typeOf>()) this.converter.write(serializableBean, ResolvableType.forMethodParameter(methodParameter), null, - outputMessage, null) + outputMessage, hints) val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index f10b85de539..bf61c668ee5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -17,7 +17,6 @@ package org.springframework.web.servlet.config.annotation; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -106,8 +105,12 @@ import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionRes import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.JsonViewRequestBodyAdvice; import org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyAdvice; +import org.springframework.web.servlet.mvc.method.annotation.KotlinRequestBodyAdvice; +import org.springframework.web.servlet.mvc.method.annotation.KotlinResponseBodyAdvice; +import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; import org.springframework.web.servlet.resource.ResourceUrlProvider; import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor; @@ -225,6 +228,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv private static final boolean jsonbPresent; + private static final boolean kotlinSerializationPresent; + private static final boolean kotlinSerializationCborPresent; private static final boolean kotlinSerializationJsonPresent; @@ -248,9 +253,10 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv jackson2YamlPresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader); gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader); - kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader); - kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader); - kotlinSerializationProtobufPresent = ClassUtils.isPresent("kotlinx.serialization.protobuf.ProtoBuf", classLoader); + kotlinSerializationPresent = ClassUtils.isPresent("kotlinx.serialization.Serializable", classLoader); + kotlinSerializationCborPresent = kotlinSerializationPresent && ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader); + kotlinSerializationJsonPresent = kotlinSerializationPresent && ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader); + kotlinSerializationProtobufPresent = kotlinSerializationPresent && ClassUtils.isPresent("kotlinx.serialization.protobuf.ProtoBuf", classLoader); } @@ -699,9 +705,19 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv adapter.setCustomReturnValueHandlers(getReturnValueHandlers()); adapter.setErrorResponseInterceptors(getErrorResponseInterceptors()); - if (jacksonPresent || jackson2Present) { - adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice())); - adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice())); + if (jacksonPresent || jackson2Present || kotlinSerializationPresent) { + List requestBodyAdvices = new ArrayList<>(2); + List> responseBodyAdvices = new ArrayList<>(2); + if (jacksonPresent || jackson2Present) { + requestBodyAdvices.add(new JsonViewRequestBodyAdvice()); + responseBodyAdvices.add(new JsonViewResponseBodyAdvice()); + } + if (kotlinSerializationPresent) { + requestBodyAdvices.add(new KotlinRequestBodyAdvice()); + responseBodyAdvices.add(new KotlinResponseBodyAdvice()); + } + adapter.setRequestBodyAdvice(requestBodyAdvices); + adapter.setResponseBodyAdvice(responseBodyAdvices); } AsyncSupportConfigurer configurer = getAsyncSupportConfigurer(); @@ -1122,9 +1138,15 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv exceptionHandlerResolver.setCustomArgumentResolvers(getArgumentResolvers()); exceptionHandlerResolver.setCustomReturnValueHandlers(getReturnValueHandlers()); exceptionHandlerResolver.setErrorResponseInterceptors(getErrorResponseInterceptors()); - if (jacksonPresent || jackson2Present) { - exceptionHandlerResolver.setResponseBodyAdvice( - Collections.singletonList(new JsonViewResponseBodyAdvice())); + if (jacksonPresent || jackson2Present || kotlinSerializationPresent) { + List> responseBodyAdvices = new ArrayList<>(2); + if (jacksonPresent || jackson2Present) { + responseBodyAdvices.add(new JsonViewResponseBodyAdvice()); + } + if (kotlinSerializationPresent) { + responseBodyAdvices.add(new KotlinResponseBodyAdvice()); + } + exceptionHandlerResolver.setResponseBodyAdvice(responseBodyAdvices); } if (this.applicationContext != null) { exceptionHandlerResolver.setApplicationContext(this.applicationContext); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/KotlinRequestBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/KotlinRequestBodyAdvice.java new file mode 100644 index 00000000000..df147ea3a2e --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/KotlinRequestBodyAdvice.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; +import kotlin.reflect.KType; +import kotlin.reflect.jvm.ReflectJvmMapping; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.MethodParameter; +import org.springframework.http.converter.AbstractKotlinSerializationHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.SmartHttpMessageConverter; + +/** + * A {@link RequestBodyAdvice} implementation that adds support for resolving + * Kotlin {@link KType} from the parameter and providing it as a hint with a + * {@code "kotlin.reflect.KType"} key. + * + * @author Sebastien Deleuze + * @since 7.0 + * @see AbstractKotlinSerializationHttpMessageConverter + */ +@SuppressWarnings("removal") +public class KotlinRequestBodyAdvice extends RequestBodyAdviceAdapter { + + @Override + public boolean supports(MethodParameter methodParameter, Type targetType, + Class> converterType) { + + return AbstractKotlinSerializationHttpMessageConverter.class.isAssignableFrom(converterType); + } + + @Override + public @Nullable Map determineReadHints(MethodParameter parameter, Type targetType, + Class> converterType) { + + KFunction function = ReflectJvmMapping.getKotlinFunction(Objects.requireNonNull(parameter.getMethod())); + int i = 0; + int index = parameter.getParameterIndex(); + for (KParameter p : Objects.requireNonNull(function).getParameters()) { + if (KParameter.Kind.VALUE.equals(p.getKind())) { + if (index == i++) { + return Collections.singletonMap(KType.class.getName(), p.getType()); + } + } + } + return null; + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/KotlinResponseBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/KotlinResponseBodyAdvice.java new file mode 100644 index 00000000000..d1661ab1686 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/KotlinResponseBodyAdvice.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +import kotlin.reflect.KFunction; +import kotlin.reflect.KType; +import kotlin.reflect.jvm.ReflectJvmMapping; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractKotlinSerializationHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; + +/** + * A {@link ResponseBodyAdvice} implementation that adds support for resolving + * Kotlin {@link KType} from the return type and providing it as a hint with a + * {@code "kotlin.reflect.KType"} key. + * + * @author Sebastien Deleuze + * @since 7.0 + */ +@SuppressWarnings("removal") +public class KotlinResponseBodyAdvice implements ResponseBodyAdvice { + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + return AbstractKotlinSerializationHttpMessageConverter.class.isAssignableFrom(converterType); + } + + @Override + public @Nullable Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType selectedContentType, + Class> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { + + return body; + } + + @Override + public @Nullable Map determineWriteHints(@Nullable Object body, MethodParameter returnType, + MediaType selectedContentType, Class> selectedConverterType) { + + KFunction function = ReflectJvmMapping.getKotlinFunction(Objects.requireNonNull(returnType.getMethod())); + KType type = Objects.requireNonNull(function).getReturnType(); + return Collections.singletonMap(KType.class.getName(), type); + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java index c96c7994ba8..e66d9316f0b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java @@ -80,6 +80,8 @@ import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionRes import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.JsonViewRequestBodyAdvice; import org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyAdvice; +import org.springframework.web.servlet.mvc.method.annotation.KotlinRequestBodyAdvice; +import org.springframework.web.servlet.mvc.method.annotation.KotlinResponseBodyAdvice; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @@ -204,9 +206,11 @@ class WebMvcConfigurationSupportTests { DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(adapter); @SuppressWarnings("unchecked") List bodyAdvice = (List) fieldAccessor.getPropertyValue("requestResponseBodyAdvice"); - assertThat(bodyAdvice).hasSize(2); + assertThat(bodyAdvice).hasSize(4); assertThat(bodyAdvice.get(0).getClass()).isEqualTo(JsonViewRequestBodyAdvice.class); - assertThat(bodyAdvice.get(1).getClass()).isEqualTo(JsonViewResponseBodyAdvice.class); + assertThat(bodyAdvice.get(1).getClass()).isEqualTo(KotlinRequestBodyAdvice.class); + assertThat(bodyAdvice.get(2).getClass()).isEqualTo(JsonViewResponseBodyAdvice.class); + assertThat(bodyAdvice.get(3).getClass()).isEqualTo(KotlinResponseBodyAdvice.class); } @Test @@ -238,8 +242,9 @@ class WebMvcConfigurationSupportTests { DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(eher); List interceptors = (List) fieldAccessor.getPropertyValue("responseBodyAdvice"); - assertThat(interceptors).hasSize(1); + assertThat(interceptors).hasSize(2); assertThat(interceptors.get(0).getClass()).isEqualTo(JsonViewResponseBodyAdvice.class); + assertThat(interceptors.get(1).getClass()).isEqualTo(KotlinResponseBodyAdvice.class); LocaleContextHolder.setLocale(Locale.ENGLISH); try { diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorKotlinTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorKotlinTests.kt index d9cc6ae770e..ffe56e450b9 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorKotlinTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorKotlinTests.kt @@ -34,6 +34,7 @@ import org.springframework.web.method.support.ModelAndViewContainer import org.springframework.web.testfixture.servlet.MockHttpServletRequest import org.springframework.web.testfixture.servlet.MockHttpServletResponse import java.nio.charset.StandardCharsets +import kotlin.collections.mapOf import kotlin.reflect.jvm.javaMethod /** @@ -84,6 +85,22 @@ class RequestResponseBodyMethodProcessorKotlinTests { .contains("\"value\":\"bar\"") } + @Test + fun writeNullableMapWithKotlinSerializationJsonMessageConverter() { + val method = SampleController::writeNullableMap::javaMethod.get()!! + val handlerMethod = HandlerMethod(SampleController(), method) + val methodReturnType = handlerMethod.returnType + + val converters = listOf(KotlinSerializationJsonHttpMessageConverter()) + val processor = RequestResponseBodyMethodProcessor(converters, null, listOf(KotlinResponseBodyAdvice())) + + val returnValue: Any = SampleController().writeNullableMap() + processor.handleReturnValue(returnValue, methodReturnType, this.container, this.request) + + Assertions.assertThat(this.servletResponse.contentAsString) + .isEqualTo("""{"value":null}""") + } + @Test fun readWithKotlinSerializationJsonMessageConverter() { val content = "{\"value\" : \"foo\"}" @@ -119,6 +136,24 @@ class RequestResponseBodyMethodProcessorKotlinTests { Assertions.assertThat(result).containsExactly(Message("foo"), Message("bar")) } + @Suppress("UNCHECKED_CAST") + @Test + fun readNullableMapWithKotlinSerializationJsonMessageConverter() { + val content = "{\"value\" : null}" + this.servletRequest.setContent(content.toByteArray(StandardCharsets.UTF_8)) + this.servletRequest.setContentType("application/json") + + val converters = listOf(StringHttpMessageConverter(), KotlinSerializationJsonHttpMessageConverter()) + val processor = RequestResponseBodyMethodProcessor(converters, null, listOf(KotlinRequestBodyAdvice())) + + val method = SampleController::readNullableMap::javaMethod.get()!! + val methodParameter = MethodParameter(method, 0) + + val result = processor.resolveArgument(methodParameter, container, request, factory) as Map + + Assertions.assertThat(result).isEqualTo(mapOf("value" to null)) + } + private class SampleController { @@ -135,7 +170,20 @@ class RequestResponseBodyMethodProcessorKotlinTests { fun readMessage(message: Message) = message.value @RequestMapping - @ResponseBody fun readMessages(messages: List) = messages.map { it.value }.reduce { acc, string -> "$acc $string" } + @ResponseBody + fun readMessages(messages: List) = messages.map { it.value }.reduce { acc, string -> "$acc $string" } + + @RequestMapping + @ResponseBody + fun writeNullableMap(): Map { + return mapOf("value" to null) + } + + @RequestMapping + @ResponseBody + fun readNullableMap(map: Map): String { + return map.toString() + } }