Description
After upgrading from Spring Framework 7.0.6 to 7.0.7, Kotlin inline value classes declared as nullable parameters (TestValueClass?) inside suspend controller methods cause an IllegalArgumentException. The error is:
java.lang.IllegalArgumentException: object of type java.lang.String is not an instance of ValueClassTestController$TestValueClass
This happens both for @RequestParam and @PathVariable binding.
In 7.0.6 the same code worked correctly; the value class was properly instantiated from the string value.
Interesting note: if the parameter is changed from nullable (TestValueClass?) to non-nullable (TestValueClass), the binding succeeds again in 7.0.7.
Steps to Reproduce
- Create a Spring WebFlux controller with a
suspend function that has a nullable inline value class parameter annotated with @RequestParam or @PathVariable.
- Use
WebTestClient to send a request with a valid string value for that parameter.
- Run under Spring Framework 7.0.6 → test passes.
- Run under 7.0.7 → test fails with
IllegalArgumentException.
Minimal reproduction
import org.junit.jupiter.api.Test
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
class ValueClassTestController {
@GetMapping(path = ["/test"])
suspend fun test1(@RequestParam param: TestValueClass?): String? {
return param?.value
}
@GetMapping(path = ["/test/{param}"])
suspend fun test2(@PathVariable param: TestValueClass?): String? {
return param?.value
}
@JvmInline
value class TestValueClass(val value: String)
}
class ValueClassControllerTest {
private val webTestClient = WebTestClient.bindToController(ValueClassTestController()).build()
@Test
fun `test RequestParam should return correct value`() {
webTestClient.get().uri("/test?param=123")
.exchange()
.expectStatus().isOk
.expectBody<String>().isEqualTo("123")
}
@Test
fun `test PathVariable should return correct value`() {
webTestClient.get().uri("/test/456")
.exchange()
.expectStatus().isOk
.expectBody<String>().isEqualTo("456")
}
}
Expected Behavior
Both tests pass; the client receives "123" and "456" respectively.
Actual Behavior (7.0.7)
Both tests fail with a 500 INTERNAL SERVER ERROR. The root cause is:
java.lang.IllegalArgumentException: object of type java.lang.String is not an instance of ValueClassTestController$TestValueClass
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.checkReceiver(DirectMethodHandleAccessor.java:199)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:100)
...
at kotlin.reflect.jvm.internal.calls.ValueClassAwareCaller.call(ValueClassAwareCaller.kt:137)
at kotlin.reflect.jvm.internal.ReflectKCallableKt.callDefaultMethod(ReflectKCallable.kt:153)
at kotlin.reflect.full.KCallables.callSuspendBy(KCallables.kt:82)
at org.springframework.core.CoroutinesUtils.lambda$invokeSuspendingFunction$1(CoroutinesUtils.java:148)
The stack trace reveals that the string value provided by the framework is passed directly to the Kotlin reflection call, but the reflection layer expects an already‑boxed value class instance. For nullable parameters, the framework incorrectly hands over the raw String instead of wrapping it in the value class (or leaving it as null).
When the parameter is changed to TestValueClass (non-nullable), the problem disappears – the binding works again.
Workaround
Avoid nullable inline value class parameters in suspend functions.
Environment
- Spring Framework version: 7.0.7 (regressed from 7.0.6)
- Kotlin version: 2.3.21
- Java version: 25
- Web stack: Spring WebFlux
- Build tool: Gradle
Additional Context
Description
After upgrading from Spring Framework 7.0.6 to 7.0.7, Kotlin inline value classes declared as nullable parameters (
TestValueClass?) insidesuspendcontroller methods cause anIllegalArgumentException. The error is:This happens both for
@RequestParamand@PathVariablebinding.In 7.0.6 the same code worked correctly; the value class was properly instantiated from the string value.
Interesting note: if the parameter is changed from nullable (
TestValueClass?) to non-nullable (TestValueClass), the binding succeeds again in 7.0.7.Steps to Reproduce
suspendfunction that has a nullable inline value class parameter annotated with@RequestParamor@PathVariable.WebTestClientto send a request with a valid string value for that parameter.IllegalArgumentException.Minimal reproduction
Expected Behavior
Both tests pass; the client receives
"123"and"456"respectively.Actual Behavior (7.0.7)
Both tests fail with a
500 INTERNAL SERVER ERROR. The root cause is:The stack trace reveals that the string value provided by the framework is passed directly to the Kotlin reflection call, but the reflection layer expects an already‑boxed value class instance. For nullable parameters, the framework incorrectly hands over the raw
Stringinstead of wrapping it in the value class (or leaving it asnull).When the parameter is changed to
TestValueClass(non-nullable), the problem disappears – the binding works again.Workaround
Avoid nullable inline value class parameters in
suspendfunctions.Environment
Additional Context
CoroutineUtils#36449.