bug: Kotlin inline value classes don't work as @InputArgument
Hello guys, New to graphql so may or may not be a configuration issue somewhere, or it might be a complicated multiparty problem.
Expected behavior
To be able to use kotlin inline value classes as custom scalar inputs and pass them as method inputs with @InputArgument.
Actual behavior
Various degrees of success, not able to use method definition as contract but dfe.getArgument works.
Steps to reproduce
// Fetcher
@DgsComponent
class SomethingFetcher {
// Errors due to value class jvm mangling
// Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'schema' defined in class path resource [com/netflix/graphql/dgs/autoconfig/DgsAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [graphql.schema.GraphQLSchema]: Factory method 'schema' threw exception; nested exception is graphql.AssertException: Name must be non-null, non-empty and match [_A-Za-z][_0-9A-Za-z]* - was 'something-ViFSl3M'
@DgsQuery
fun something(@InputArgument("color") color: Color?): Int = 1
/**
* {
* "errors": [
* {
* "message": "com.netflix.graphql.dgs.exceptions.DgsInvalidInputArgumentException: Specified type 'class java.lang.String' is invalid. Found com.example.valuescalar.backend.graphql.model.Color instead.",
* "locations": [],
* "path": [
* "something"
* ],
* "extensions": {
* "errorType": "INTERNAL"
* }
* }
* ],
* "data": {
* "teams": null
* }
* }
*/
@DgsQuery
@Suppress("INAPPLICABLE_JVM_NAME") // Needed as @Component from spring stops @JvmName from being usable, not sure why this is happening in the first place
@JvmName("something")
fun something(@InputArgument("color") color: Color): Int = 1
// WORKS but less nice
@DgsQuery
fun something(dataFetchingEnvironment: DataFetchingEnvironment): Int {
val color: Color = dataFetchingEnvironment.getArgument("color")
return 1
}
}
// Color
@JvmInline
value class Color(val value: String) {
init {
require(value.matches("^#[0-9A-F]{6}$".toRegex()))
}
override fun toString(): String = value
}
// Scalar coercing
@DgsScalar(name = "Color")
internal class ColorScalar : Coercing<Color, String> {
@Throws(CoercingParseValueException::class)
override fun parseValue(input: Any): Color =
runCatching { Color(serialize(input)) }
.getOrElse { throw CoercingParseValueException("Invalid color.", it) }
@Throws(CoercingParseLiteralException::class)
override fun parseLiteral(input: Any): Color {
val colorString = (input as? StringValue)?.value ?: throw CoercingParseLiteralException("Invalid color.")
return runCatching { Color(colorString) }
.getOrElse { throw CoercingParseLiteralException("Invalid color.", it) }
}
override fun serialize(dataFetcherResult: Any): String = dataFetcherResult.toString()
}
# Schema
type Query {
something(color: Color): Int
}
scalar Color
// build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.4.5"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.5.0"
kotlin("plugin.spring") version "1.5.0"
id("com.netflix.dgs.codegen") version "4.6.4"
}
group = "com.example.valuescalar"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation(platform("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:3.12.1"))
implementation("com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks {
withType<com.netflix.graphql.dgs.codegen.gradle.GenerateJavaTask> {
schemaPaths = mutableListOf("${projectDir}/src/main/resources/schema")
packageName = "com.example.valuescalar.backend.graphql.generated"
typeMapping = mutableMapOf(
"Color" to "com.example.valuescalar.backend.graphql.model.Color"
)
}
withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
withType<Test> {
useJUnitPlatform()
}
}
Thanks for reporting, will look into it.
I looked into this a bit; the issue seems to be that we're using Java reflection (not Kotlin reflection) to work with input arguments.
If you define an argument like this:
@DgsData(parentType = "Query", field = "hello")
fun someFetcher(@InputArgument color: Color): String {
return color.toString()
}
The Color argument is seen as String by reflection.
To get this to work we'll have to use Kotlin reflection instead. We might be able to switch to Kotlin reflection entirely, but we'll have to experiment with that further.