spring-graphql icon indicating copy to clipboard operation
spring-graphql copied to clipboard

Support for Kotlin inline value classes

Open dgerhardt opened this issue 10 months ago • 5 comments

Currently, Spring for GraphQL does not work with Kotlin inline value classes. When these are used as field types, I have noticed two issues:

  1. Schema mappings do no longer work out of the box. The cause is probably that the Kotlin compiler mangles method names to avoid conflicts (https://kotlinlang.org/docs/inline-classes.html#mangling). This can be worked around by using @JvmName or explicit names with @SchemaMapping.
  2. Spring for GraphQL is no longer able to use constructor binding for classes with value class fields. This seems to be caused by DefaultConstructorMarker which the Kotlin compiler adds to the constructor's signature. I have not found any workaround for this to make this work with Spring for GraphQL.

Spring Data already already supports value classes since version 3.2. It would be great if Spring for GraphQL would support them too, so they can be used throughout the whole Spring stack.

Related Spring Data PR: https://github.com/spring-projects/spring-data-commons/pull/2866 Related Spring Data docs: https://docs.spring.io/spring-data/commons/reference/object-mapping.html#mapping.kotlin.value.classes

Exception when constructor binding with a value class:

java.lang.IllegalStateException: Invalid number of parameter names: 8 for constructor public com.example.Entity(java.util.UUID,[...],kotlin.jvm.internal.DefaultConstructorMarker)
        at org.springframework.util.Assert.state(Assert.java:101) ~[spring-core-6.2.5.jar:6.2.5]
        at org.springframework.beans.BeanUtils.getParameterNames(BeanUtils.java:672) ~[spring-beans-6.2.5.jar:6.2.5]
        at org.springframework.graphql.data.GraphQlArgumentBinder.bindMapToObjectViaConstructor(GraphQlArgumentBinder.java:295) ~[spring-graphql-1.3.4.jar:1.3.4]
        at org.springframework.graphql.data.GraphQlArgumentBinder.bindMap(GraphQlArgumentBinder.java:261) ~[spring-graphql-1.3.4.jar:1.3.4]

dgerhardt avatar Apr 10 '25 10:04 dgerhardt

Can you explain the difference with https://github.com/spring-projects/spring-graphql/issues/1163 please?

bclozel avatar Apr 10 '25 11:04 bclozel

@bclozel I don't think the issues are related. While both are related to constructors and Kotlin, this issue is about the use of value classes while the existing issue is about using a constructor for only some properties and setters for others. A fix for #1163 might provide a workaround for value classes but I'm not entirely sure.

dgerhardt avatar Apr 10 '25 12:04 dgerhardt

Rather than pointing to other PRs, can you share a code snippet or a minimal reproducer? It seems Spring Framework does support inline/value classes in BeanUtils and we are using just that method to find the constructor. I guess we're missing something else in the binding process but I'm not sure what.

bclozel avatar Apr 15 '25 13:04 bclozel

I've narrowed this down to a problem in Spring Framework. I've opened https://github.com/spring-projects/spring-framework/issues/34760

There are many subtle issues with Kotlin<->Java reflection and I'm not sure this use case will be supported in Spring for GraphQL 1.x. We might wait for broader support in Spring Framework 7.0 with https://github.com/spring-projects/spring-framework/issues/33630

bclozel avatar Apr 15 '25 14:04 bclozel

Thanks for looking into this so quickly. I'm looking forward to broader support in Spring Framework 7.0.

dgerhardt avatar Apr 16 '25 07:04 dgerhardt

https://github.com/spring-projects/spring-framework/issues/34760 has been fixed but unfortunately we're hitting another problem. We cannot reflectively instantiate data classes with value class arguments (and probably default argument values).

This does not work:

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.BeanUtils
import java.lang.reflect.Constructor

class ValueclassApplicationTests {

	@Test
	fun instantiateWithArgumentFails() {
        val constructor = MyDataClass::class.java.constructors.first() as Constructor<MyDataClass>;
        assertThat(constructor.parameterTypes).contains(String::class.java).doesNotContain(MyValueClass::class.java)
        val myDataClass = BeanUtils.instantiateClass<MyDataClass>(constructor, "firstValue", "secondValue");
        assertThat(myDataClass.second.value).isEqualTo("secondValue")
	}

    @Test
    fun instantiateWithValueClassArgumentSuccess() {
        val constructor = MyDataClass::class.java.constructors.first() as Constructor<MyDataClass>;
        assertThat(constructor.parameterTypes).contains(String::class.java).doesNotContain(MyValueClass::class.java)
        val myDataClass = BeanUtils.instantiateClass<MyDataClass>(constructor, "firstValue", MyValueClass("secondValue"));
        assertThat(myDataClass.second.value).isEqualTo("secondValue")
    }

    data class MyDataClass(val first: String, val second: MyValueClass)

    @JvmInline
    value class MyValueClass(val value: String) {

    }

}

I have created a minimal repro for future reference: https://github.com/bclozel/graphql-1186

This looks definitely in the scope of https://github.com/spring-projects/spring-framework/issues/33630, as there is existing (bot not shared) support in Spring Framework with InvocableHandlerMethod.

I don't think support will be available in the 1.4.x generation, hopefully we'll have something in time for our 2.0.x generation.

bclozel avatar Aug 29 '25 16:08 bclozel

I have found a way to support this case in 1.4.x. We will probably find new limitations along the way. We might be able to work around those, but full support should be expected with #1299 when Framework provides the infrastructure.

@dgerhardt you can check out 1.4.2-SNAPSHOT and let me know if this works for you. If it does not, please provide a code snippet with the bound argument type and all the types of its attributes so we can work on a test. Thanks!

bclozel avatar Aug 30 '25 10:08 bclozel