diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java
index 35d9a1be2a5..96fac2afbf0 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyInserters.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -103,6 +103,32 @@ public abstract class BodyInserters {
writeWithMessageWriters(message, context, Mono.just(body), ResolvableType.forInstance(body), null);
}
+ /**
+ * Inserter to write the given value.
+ *
Alternatively, consider using the {@code bodyValue(Object, ParameterizedTypeReference)} shortcuts on
+ * {@link org.springframework.web.reactive.function.client.WebClient WebClient} and
+ * {@link org.springframework.web.reactive.function.server.ServerResponse ServerResponse}.
+ * @param body the value to write
+ * @param bodyType the type of the body, used to capture the generic type
+ * @param the type of the body
+ * @return the inserter to write a single value
+ * @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an
+ * instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()},
+ * for which {@link #fromPublisher(Publisher, ParameterizedTypeReference)} or
+ * {@link #fromProducer(Object, ParameterizedTypeReference)} should be used.
+ * @since 6.2
+ * @see #fromPublisher(Publisher, ParameterizedTypeReference)
+ * @see #fromProducer(Object, ParameterizedTypeReference)
+ */
+ public static BodyInserter fromValue(T body, ParameterizedTypeReference bodyType) {
+ Assert.notNull(body, "'body' must not be null");
+ Assert.notNull(bodyType, "'bodyType' must not be null");
+ Assert.isNull(registry.getAdapter(body.getClass()),
+ "'body' should be an object, for reactive types use a variant specifying a publisher/producer and its related element type");
+ return (message, context) ->
+ writeWithMessageWriters(message, context, Mono.just(body), ResolvableType.forType(bodyType), null);
+ }
+
/**
* Inserter to write the given object.
* Alternatively, consider using the {@code bodyValue(Object)} shortcuts on
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java
index 6308ddf5a9f..01ea08c5098 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java
@@ -367,6 +367,12 @@ final class DefaultWebClient implements WebClient {
return this;
}
+ @Override
+ public RequestHeadersSpec> bodyValue(T body, ParameterizedTypeReference bodyType) {
+ this.inserter = BodyInserters.fromValue(body, bodyType);
+ return this;
+ }
+
@Override
public > RequestHeadersSpec> body(
P publisher, ParameterizedTypeReference elementTypeRef) {
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java
index 02586478e33..ffb454b7626 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java
@@ -692,9 +692,38 @@ public interface WebClient {
* @throws IllegalArgumentException if {@code body} is a
* {@link Publisher} or producer known to {@link ReactiveAdapterRegistry}
* @since 5.2
+ * @see #bodyValue(Object, ParameterizedTypeReference)
*/
RequestHeadersSpec> bodyValue(Object body);
+ /**
+ * Shortcut for {@link #body(BodyInserter)} with a
+ * {@linkplain BodyInserters#fromValue value inserter}.
+ * For example:
+ *
+ * List<Person> list = ... ;
+ *
+ * Mono<Void> result = client.post()
+ * .uri("/persons/{id}", id)
+ * .contentType(MediaType.APPLICATION_JSON)
+ * .bodyValue(list, new ParameterizedTypeReference<List<Person>>() {};)
+ * .retrieve()
+ * .bodyToMono(Void.class);
+ *
+ * For multipart requests consider providing
+ * {@link org.springframework.util.MultiValueMap MultiValueMap} prepared
+ * with {@link org.springframework.http.client.MultipartBodyBuilder
+ * MultipartBodyBuilder}.
+ * @param body the value to write to the request body
+ * @param bodyType the type of the body, used to capture the generic type
+ * @param the type of the body
+ * @return this builder
+ * @throws IllegalArgumentException if {@code body} is a
+ * {@link Publisher} or producer known to {@link ReactiveAdapterRegistry}
+ * @since 6.2
+ */
+ RequestHeadersSpec> bodyValue(T body, ParameterizedTypeReference bodyType);
+
/**
* Shortcut for {@link #body(BodyInserter)} with a
* {@linkplain BodyInserters#fromPublisher Publisher inserter}.
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java
index 701854fe06a..9f0df3b736d 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -225,6 +225,11 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder {
return initBuilder(body, BodyInserters.fromValue(body));
}
+ @Override
+ public Mono bodyValue(T body, ParameterizedTypeReference bodyType) {
+ return initBuilder(body, BodyInserters.fromValue(body, bodyType));
+ }
+
@Override
public > Mono body(P publisher, Class elementClass) {
return initBuilder(publisher, BodyInserters.fromPublisher(publisher, elementClass));
diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java
index 2bb5c9454b0..f10d770ad2f 100644
--- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java
+++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ServerResponse.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -420,6 +420,20 @@ public interface ServerResponse {
*/
Mono bodyValue(Object body);
+ /**
+ * Set the body of the response to the given {@code Object} and return it.
+ * This is a shortcut for using a {@link #body(BodyInserter)} with a
+ * {@linkplain BodyInserters#fromValue value inserter}.
+ * @param body the body of the response
+ * @param bodyType the type of the body, used to capture the generic type
+ * @param the type of the body
+ * @return the built response
+ * @throws IllegalArgumentException if {@code body} is a
+ * {@link Publisher} or producer known to {@link ReactiveAdapterRegistry}
+ * @since 6.2
+ */
+ Mono bodyValue(T body, ParameterizedTypeReference bodyType);
+
/**
* Set the body from the given {@code Publisher}. Shortcut for
* {@link #body(BodyInserter)} with a
diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt
index ce917db7d91..375eb16a1e9 100644
--- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt
+++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -68,6 +68,18 @@ inline fun RequestBodySpec.body(flow: Flow): RequestHeaders
inline fun RequestBodySpec.body(producer: Any): RequestHeadersSpec<*> =
body(producer, object : ParameterizedTypeReference() {})
+/**
+ * Extension for [WebClient.RequestBodySpec.bodyValue] providing a `bodyValueWithType(Any)` variant
+ * leveraging Kotlin reified type parameters. This extension is not subject to type
+ * erasure and retains actual generic type arguments.
+ * @param body the value to write to the request body
+ * @param T the type of the body
+ * @author Sebastien Deleuze
+ * @since 6.2
+ */
+inline fun RequestBodySpec.bodyValueWithType(body: T): RequestHeadersSpec<*> =
+ bodyValue(body, object : ParameterizedTypeReference() {})
+
/**
* Coroutines variant of [WebClient.RequestHeadersSpec.exchange].
*
diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt
index 4411105ce27..b3d6a9a15c4 100644
--- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt
+++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensions.kt
@@ -51,9 +51,6 @@ inline fun ServerResponse.BodyBuilder.body(producer: Any): Mon
/**
* Coroutines variant of [ServerResponse.BodyBuilder.bodyValue].
*
- * Set the body of the response to the given {@code Object} and return it.
- * This convenience method combines [body] and
- * [org.springframework.web.reactive.function.BodyInserters.fromValue].
* @param body the body of the response
* @return the built response
* @throws IllegalArgumentException if `body` is a [Publisher] or an
@@ -62,6 +59,21 @@ inline fun ServerResponse.BodyBuilder.body(producer: Any): Mon
suspend fun ServerResponse.BodyBuilder.bodyValueAndAwait(body: Any): ServerResponse =
bodyValue(body).awaitSingle()
+/**
+ * Coroutines variant of [ServerResponse.BodyBuilder.bodyValue] providing a `bodyValueWithTypeAndAwait(Any)` variant
+ * leveraging Kotlin reified type parameters. This extension is not subject to type
+ * erasure and retains actual generic type arguments.
+ *
+ * @param body the body of the response
+ * @param T the type of the body
+ * @return the built response
+ * @throws IllegalArgumentException if `body` is a [Publisher] or an
+ * instance of a type supported by [org.springframework.core.ReactiveAdapterRegistry.getSharedInstance],
+ * @since 6.2
+ */
+suspend inline fun ServerResponse.BodyBuilder.bodyValueWithTypeAndAwait(body: T): ServerResponse =
+ bodyValue(body, object : ParameterizedTypeReference() {}).awaitSingle()
+
/**
* Coroutines variant of [ServerResponse.BodyBuilder.body] with [Any] and
* [ParameterizedTypeReference] parameters providing a `bodyAndAwait(Flow)` variant.
diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/KotlinBodyInsertersTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/KotlinBodyInsertersTests.kt
new file mode 100644
index 00000000000..d0bc01ceeef
--- /dev/null
+++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/KotlinBodyInsertersTests.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2002-2024 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.reactive.function
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.springframework.core.ParameterizedTypeReference
+import org.springframework.http.codec.EncoderHttpMessageWriter
+import org.springframework.http.codec.HttpMessageWriter
+import org.springframework.http.codec.json.KotlinSerializationJsonEncoder
+import org.springframework.http.server.reactive.ServerHttpRequest
+import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse
+import reactor.test.StepVerifier
+import java.util.*
+
+/**
+ * @author Sebastien Deleuze
+ */
+class KotlinBodyInsertersTests {
+
+ private lateinit var context: BodyInserter.Context
+
+ private lateinit var hints: Map
+
+
+ @BeforeEach
+ fun createContext() {
+ val messageWriters: MutableList> = ArrayList()
+ val jsonEncoder = KotlinSerializationJsonEncoder()
+ messageWriters.add(EncoderHttpMessageWriter(jsonEncoder))
+
+ this.context = object : BodyInserter.Context {
+ override fun messageWriters(): List> {
+ return messageWriters
+ }
+
+ override fun serverRequest(): Optional {
+ return Optional.empty()
+ }
+
+ override fun hints(): Map {
+ return hints
+ }
+ }
+ this.hints = HashMap()
+ }
+
+ @Test
+ fun ofObjectWithBodyType() {
+ val somebody = SomeBody(1, "name")
+ val body = listOf(somebody)
+ val inserter = BodyInserters.fromValue(body, object: ParameterizedTypeReference>() {})
+ val response = MockServerHttpResponse()
+ val result = inserter.insert(response, context)
+ StepVerifier.create(result).expectComplete().verify()
+
+ StepVerifier.create(response.bodyAsString)
+ .expectNext("[{\"user_id\":1,\"name\":\"name\"}]")
+ .expectComplete()
+ .verify()
+ }
+
+ @Serializable
+ data class SomeBody(@SerialName("user_id") val userId: Int, val name: String)
+}
diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt
index 7bca596a010..9d6ab8e1185 100644
--- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt
+++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -66,6 +66,13 @@ class WebClientExtensionsTests {
verify { requestBodySpec.body(ofType(), object : ParameterizedTypeReference>() {}) }
}
+ @Test
+ fun `RequestBodySpec#bodyValueWithType with reified type parameters`() {
+ val body = mockk>()
+ requestBodySpec.bodyValueWithType>(body)
+ verify { requestBodySpec.bodyValue(body, object : ParameterizedTypeReference>() {}) }
+ }
+
@Test
fun `ResponseSpec#bodyToMono with reified type parameters`() {
responseSpec.bodyToMono>()
diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt
index a4455cf1c11..7a9648b83e0 100644
--- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt
+++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/ServerResponseExtensionsTests.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -75,6 +75,19 @@ class ServerResponseExtensionsTests {
}
}
+ @Test
+ fun `BodyBuilder#bodyValueWithTypeAndAwait with object parameter and reified type parameters`() {
+ val response = mockk()
+ val body = listOf("foo", "bar")
+ every { bodyBuilder.bodyValue(ofType>(), object : ParameterizedTypeReference>() {}) } returns Mono.just(response)
+ runBlocking {
+ bodyBuilder.bodyValueWithTypeAndAwait>(body)
+ }
+ verify {
+ bodyBuilder.bodyValue(body, object : ParameterizedTypeReference>() {})
+ }
+ }
+
@Test
fun `BodyBuilder#bodyAndAwait with flow parameter`() {
val response = mockk()