Fix HttpEntity support with Kotlin Serialization

This commit adds HttpEntity type unwrapping logic to
KotlinRequestBodyAdvice and KotlinResponseBodyAdvice.

Closes gh-35281
This commit is contained in:
Sébastien Deleuze 2025-08-18 11:53:38 +02:00
parent 0d2a0d7b9e
commit 3dc2aa79a4
3 changed files with 53 additions and 0 deletions

View File

@ -28,6 +28,7 @@ import kotlin.reflect.jvm.ReflectJvmMapping;
import org.jspecify.annotations.Nullable;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpEntity;
import org.springframework.http.converter.AbstractKotlinSerializationHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.SmartHttpMessageConverter;
@ -61,6 +62,10 @@ public class KotlinRequestBodyAdvice extends RequestBodyAdviceAdapter {
for (KParameter p : Objects.requireNonNull(function).getParameters()) {
if (KParameter.Kind.VALUE.equals(p.getKind())) {
if (index == i++) {
if (HttpEntity.class.isAssignableFrom(parameter.getParameterType())) {
return Collections.singletonMap(KType.class.getName(),
Objects.requireNonNull(p.getType().getArguments().get(0).getType()));
}
return Collections.singletonMap(KType.class.getName(), p.getType());
}
}

View File

@ -26,6 +26,7 @@ import kotlin.reflect.jvm.ReflectJvmMapping;
import org.jspecify.annotations.Nullable;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpEntity;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractKotlinSerializationHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
@ -61,6 +62,9 @@ public class KotlinResponseBodyAdvice implements ResponseBodyAdvice<Object> {
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(Objects.requireNonNull(returnType.getMethod()));
KType type = Objects.requireNonNull(function).getReturnType();
if (HttpEntity.class.isAssignableFrom(returnType.getParameterType())) {
return Collections.singletonMap(KType.class.getName(), Objects.requireNonNull(type.getArguments().get(0).getType()));
}
return Collections.singletonMap(KType.class.getName(), type);
}

View File

@ -20,6 +20,8 @@ import kotlinx.serialization.Serializable
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.core.MethodParameter
import org.springframework.http.RequestEntity
import org.springframework.http.ResponseEntity
import org.springframework.http.converter.StringHttpMessageConverter
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean
@ -68,6 +70,22 @@ class RequestResponseBodyMethodProcessorKotlinTests {
.contains("\"value\":\"foo\"")
}
@Test
fun writeEntityWithKotlinSerializationJsonMessageConverter() {
val method = SampleController::writeMessageEntity::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().writeMessageEntity().body
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()!!
@ -118,6 +136,24 @@ class RequestResponseBodyMethodProcessorKotlinTests {
Assertions.assertThat(result).isEqualTo(Message("foo"))
}
@Test
@Suppress("UNCHECKED_CAST")
fun readEntityWithKotlinSerializationJsonMessageConverter() {
val content = "{\"value\" : \"foo\"}"
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::readMessageEntity::javaMethod.get()!!
val methodParameter = MethodParameter(method, 0)
val result = processor.resolveArgument(methodParameter, container, request, factory) as Message
Assertions.assertThat(result).isEqualTo(Message("foo"))
}
@Suppress("UNCHECKED_CAST")
@Test
fun readGenericTypeWithKotlinSerializationJsonMessageConverter() {
@ -161,6 +197,10 @@ class RequestResponseBodyMethodProcessorKotlinTests {
@ResponseBody
fun writeMessage() = Message("foo")
@RequestMapping
@ResponseBody
fun writeMessageEntity() = ResponseEntity.ok(Message("foo"))
@RequestMapping
@ResponseBody
fun writeMessages() = listOf(Message("foo"), Message("bar"))
@ -169,6 +209,10 @@ class RequestResponseBodyMethodProcessorKotlinTests {
@ResponseBody
fun readMessage(message: Message) = message.value
@RequestMapping
@ResponseBody
fun readMessageEntity(entity: RequestEntity<Message>) = entity.body!!.value
@RequestMapping
@ResponseBody
fun readMessages(messages: List<Message>) = messages.map { it.value }.reduce { acc, string -> "$acc $string" }