I've encountered a bug in Spring Framework version 6.1.0 related to Kotlin value classes in web endpoints. The issue arises when a Kotlin value class is used as a path variable in a Spring Boot controller. This results in a failure to correctly handle the value type, causing runtime exceptions.

Environment

Spring Framework Version: 6.1.0 (with spring-boot 3.2.0) Kotlin Version: 1.9.20 JVM Version: 17

Steps to reproduce

  1. Define a Spring Boot controller with two endpoints, one accepting a Kotlin data class as a path variable and another accepting a Kotlin value class.
  2. Create tests for these endpoints using SpringBootTest.
  3. Observe that the endpoint with the Kotlin data class functions correctly, while the one with the kotlin value class fails.

Expected behavior

Both endpoints should accept their respective path variables without any issue

Actual Behavior

The endpoint with the Kotlin value class as a path variable fails at runtime with the error message "object is not an instance of declaring class".

Minimal example

@RestController
@RequestMapping(value = ["/exhibit"])
class KotlinCallByBugController {

    @GetMapping("/working-id/{id}")
    fun works(
        @PathVariable id: WorkingId
    ) = Unit

    @GetMapping("/value-id/{id}")
    fun broken(
        @PathVariable id: SomeId
    ) = Unit
}

@JvmInline // "Value classes without @JvmInline annotation are not supported yet"
value class SomeId(val s: String)

// data classes work just fine
data class WorkingId(val s: String)
@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    classes = [MyApplication::class])
class KotlinCallByBugControllerTest {
    @LocalServerPort
    protected val port = 0

    @Test
    fun `this works - call endpoint with path variable as data class`() {
        val client = HttpClients.createDefault()
        client.execute(HttpGet("http://localhost:$port/exhibit/working-id/123")) { response ->
            assertThat(response.code).isEqualTo(200)
        }
    }

    @Test
    fun `this does not work - call endpoint with path variable as value class`() {
        // breaks: "object is not an instance of declaring class"
        val client = HttpClients.createDefault()
        client.execute(HttpGet("http://localhost:$port/exhibit/value-id/123")) { response ->
            assertThat(response.code).isEqualTo(200)
        }
    }
}

Comment From: CharlyRien

Hello,

I wanted to mention that I am experiencing the same issue, even though I am not directly using the value class in the controller path variable. If I invoke any bean methods with a value class in the parameters, the outcome remains the same: I receive the error message object is not an instance of declaring class from java.base/jdk.internal.reflect.DirectMethodHandleAccessor.checkReceiver(DirectMethodHandleAccessor.java:197).

We are currently utilizing Java 21 on our end.

Thanks

Comment From: sdeleuze

It looks like Kotlin reflection does not support kotlin.reflect.KCallable#callBy invocation with the data class unwrapped type, I have asked Kotlin team feedback.

Comment From: sdeleuze

Confirmed to be a Kotlin bug, you can follow the resolution of https://youtrack.jetbrains.com/issue/KT-64097.

Comment From: junkdog

While not an ideal solution, one possible workaround for the Kotlin callBy bug could be to manually box arguments that are value class types. One can determine which KParameters correspond to value classes by checking the KClass::isValue property.

A simpler mitigation might be to fallback on the java reflection code path if callBy encounters failures (alt if any KParameters point to value classes) - since it is only aware of the underlying type.

Edit: Just to clarify. We upgraded from 6.0.13, where it worked fine.

Comment From: sdeleuze

Based on latest Kotlin team feedback, we may have to handle that on Spring side (in a way close to what you suggested @junkdog). See https://github.com/sdeleuze/spring-framework/commit/gh-31698 draft commit.

Comment From: koo-taejin

@sdeleuze

Since spring-boot 3.2.1, we have noticed a significant performance drop in our application. A colleague of mine (@koisyu) have tried to figure out the cause. He had analyzed that loading the Java class every time while invoking getClassifier() from KType was the reason. I think that if cache these parts, spring-framework can use this feature with similar performance as before.

Could you please take a look at this?

Thanks 🙇

Comment From: sdeleuze

Could you please share an indication of the impact you see, for example % of drop of the throughput observed for example, with how much parameters are used.

Comment From: koo-taejin

This is a very simple Kotlin-based controller method that makes it easy to test.

@GetMapping("/message")
fun message(message1: String, message2: String, message3: String): String {
    return "$message1 $message2 $message3"
}

benchmark

  • spring-boot 3.2.0
[~/Workspaces/spring]$ hey -n 100000 -c 1 http://localhost:8080/message\?message1\=hello\&message2\=hi\&message3\=thanks

Summary:
  Total:    13.5588 secs
  Slowest:  0.0098 secs
  Fastest:  0.0001 secs
  Average:  0.0001 secs
  Requests/sec: 7375.3008
  • spring-boot 3.2.1
[~/Workspaces/spring]$ hey -n 100000 -c 1 http://localhost:8080/message\?message1\=hello\&message2\=hi\&message3\=thanks

Summary:
  Total:    14.9566 secs
  Slowest:  0.1087 secs
  Fastest:  0.0001 secs
  Average:  0.0001 secs
  Requests/sec: 6686.0123

async-profiler

  • spring-boot 3.2.0 Spring Unable to use kotlin value classes as function arguments to functions mapping web endpoints

  • spring-boot 3.2.1 Spring Unable to use kotlin value classes as function arguments to functions mapping web endpoints

Let me know if you need more information. 😄

Comment From: sdeleuze

@koo-taejin I confirm a significant slowdown with my own tests, could you please create a new dedicated related issue, reusing the informations you shared above?