From a439e9030f62b60424d70dca94c38b099f8ef93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 5 Jun 2025 12:02:18 +0200 Subject: [PATCH] Fix collection support in AbstractKotlinSerializationHttpMessageConverter AbstractKotlinSerializationHttpMessageConverter#getSupportedMediaTypes(Class) currently invokes transitively supports(Class) which always return false with generic types. This commit adds an override that just invokes getSupportedMediaTypes(). Closes gh-34992 --- ...tlinSerializationHttpMessageConverter.java | 6 + spring-webmvc/spring-webmvc.gradle | 1 + ...tResponseBodyMethodProcessorKotlinTests.kt | 133 ++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorKotlinTests.kt 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 e429ae5fe0..c31efc411e 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 @@ -20,6 +20,7 @@ import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -76,6 +77,11 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter getSupportedMediaTypes(Class clazz) { + return getSupportedMediaTypes(); + } + @Override protected boolean supports(Class clazz) { return serializer(ResolvableType.forClass(clazz)) != null; diff --git a/spring-webmvc/spring-webmvc.gradle b/spring-webmvc/spring-webmvc.gradle index 4083584a37..886150e9df 100644 --- a/spring-webmvc/spring-webmvc.gradle +++ b/spring-webmvc/spring-webmvc.gradle @@ -1,6 +1,7 @@ description = "Spring Web MVC" apply plugin: "kotlin" +apply plugin: "kotlinx-serialization" dependencies { api(project(":spring-aop")) 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 new file mode 100644 index 0000000000..335803cc6e --- /dev/null +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorKotlinTests.kt @@ -0,0 +1,133 @@ +/* + * Copyright 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 kotlinx.serialization.Serializable +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.http.converter.StringHttpMessageConverter +import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseBody +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.context.request.ServletWebRequest +import org.springframework.web.method.HandlerMethod +import org.springframework.web.method.support.ModelAndViewContainer +import org.springframework.web.testfixture.servlet.MockHttpServletRequest +import org.springframework.web.testfixture.servlet.MockHttpServletResponse +import kotlin.reflect.jvm.javaMethod + +/** + * Kotlin tests for [RequestResponseBodyMethodProcessor]. + */ +class RequestResponseBodyMethodProcessorKotlinTests { + + private val container = ModelAndViewContainer() + + private val servletRequest = MockHttpServletRequest() + + private val servletResponse = MockHttpServletResponse() + + private val request: NativeWebRequest = ServletWebRequest(servletRequest, servletResponse) + + @Test + fun writeWithKotlinSerializationJsonMessageConverter() { + val method = SampleController::writeMessage::javaMethod.get()!! + val handlerMethod = HandlerMethod(SampleController(), method) + val methodReturnType = handlerMethod.returnType + + val converters = listOf(KotlinSerializationJsonHttpMessageConverter()) + val processor = RequestResponseBodyMethodProcessor(converters, null, null) + + val returnValue: Any = SampleController().writeMessage() + processor.handleReturnValue(returnValue, methodReturnType, this.container, this.request) + + Assertions.assertThat(this.servletResponse.contentAsString) + .contains("\"value\":\"foo\"") + } + + @Test + fun writeGenericTypeWithKotlinSerializationJsonMessageConverter() { + val method = SampleController::writeMessages::javaMethod.get()!! + val handlerMethod = HandlerMethod(SampleController(), method) + val methodReturnType = handlerMethod.returnType + + val converters = listOf(KotlinSerializationJsonHttpMessageConverter()) + val processor = RequestResponseBodyMethodProcessor(converters, null, null) + + val returnValue: Any = SampleController().writeMessages() + processor.handleReturnValue(returnValue, methodReturnType, this.container, this.request) + + Assertions.assertThat(this.servletResponse.contentAsString) + .contains("\"value\":\"foo\"") + .contains("\"value\":\"bar\"") + } + + @Test + fun readWithKotlinSerializationJsonMessageConverter() { + val method = SampleController::readMessage::javaMethod.get()!! + val handlerMethod = HandlerMethod(SampleController(), method) + val methodReturnType = handlerMethod.returnType + + val converters = listOf(StringHttpMessageConverter(), KotlinSerializationJsonHttpMessageConverter()) + val processor = RequestResponseBodyMethodProcessor(converters, null, null) + + val returnValue: Any = SampleController().readMessage(Message("foo")) + processor.handleReturnValue(returnValue, methodReturnType, this.container, this.request) + + Assertions.assertThat(this.servletResponse.contentAsString).isEqualTo("foo") + } + + @Test + fun readGenericTypeWithKotlinSerializationJsonMessageConverter() { + val method = SampleController::readMessages::javaMethod.get()!! + val handlerMethod = HandlerMethod(SampleController(), method) + val methodReturnType = handlerMethod.returnType + + val converters = listOf(StringHttpMessageConverter(), KotlinSerializationJsonHttpMessageConverter()) + val processor = RequestResponseBodyMethodProcessor(converters, null, null) + + val returnValue: Any = SampleController().readMessages(listOf(Message("foo"), Message("bar"))) + processor.handleReturnValue(returnValue, methodReturnType, this.container, this.request) + + Assertions.assertThat(this.servletResponse.contentAsString) + .isEqualTo("foo bar") + } + + + private class SampleController { + + @RequestMapping + @ResponseBody + fun writeMessage() = Message("foo") + + @RequestMapping + @ResponseBody + fun writeMessages() = listOf(Message("foo"), Message("bar")) + + @RequestMapping + @ResponseBody + fun readMessage(message: Message) = message.value + + @RequestMapping + @ResponseBody fun readMessages(messages: List) = messages.map { it.value }.reduce { acc, string -> "$acc $string" } + + } + + @Serializable + data class Message(val value: String) +} \ No newline at end of file