diff --git a/spring-graphql/build.gradle b/spring-graphql/build.gradle index 9dfeef87a..0d5653fe2 100644 --- a/spring-graphql/build.gradle +++ b/spring-graphql/build.gradle @@ -1,3 +1,6 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.5.31' +} description = "GraphQL Support for Spring Applications" dependencies { @@ -34,6 +37,7 @@ dependencies { testRuntimeOnly 'org.apache.logging.log4j:log4j-core' testRuntimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } test { @@ -42,3 +46,16 @@ test { events "passed", "skipped", "failed" } } +repositories { + mavenCentral() +} +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolver.java index f8950e9b7..f37ee5046 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolver.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolver.java @@ -18,6 +18,7 @@ import java.lang.reflect.Constructor; import java.util.Collection; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Stack; @@ -107,13 +108,32 @@ private Object convert(Object rawValue, Class targetType) { Object target; if (rawValue instanceof Map) { Constructor ctor = BeanUtils.getResolvableConstructor(targetType); - target = BeanUtils.instantiateClass(ctor); - DataBinder dataBinder = new DataBinder(target); - Assert.isTrue(ctor.getParameterCount() == 0, - () -> "Argument of type [" + targetType.getName() + - "] cannot be instantiated because of missing default constructor."); - MutablePropertyValues mpvs = extractPropertyValues((Map) rawValue); - dataBinder.bind(mpvs); + MutablePropertyValues propertyValues = extractPropertyValues((Map) rawValue); + + if (ctor.getParameterCount() == 0) { + target = BeanUtils.instantiateClass(ctor); + DataBinder dataBinder = new DataBinder(target); + dataBinder.bind(propertyValues); + } else { + // Data class constructor + DataBinder binder = new DataBinder(null); + String[] paramNames = BeanUtils.getParameterNames(ctor); + Class[] paramTypes = ctor.getParameterTypes(); + Object[] args = new Object[paramTypes.length]; + for (int i = 0; i < paramNames.length; i++) { + String paramName = paramNames[i]; + Object value = propertyValues.get(paramName); + value = (value instanceof List ? ((List) value).toArray() : value); + MethodParameter methodParam = new MethodParameter(ctor, i); + if (value == null && methodParam.isOptional()) { + args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null); + } + else { + args[i] = binder.convertIfNecessary(value, paramTypes[i], methodParam); + } + } + target = BeanUtils.instantiateClass(ctor, args); + } } else if (targetType.isAssignableFrom(rawValue.getClass())) { return returnValue(rawValue, targetType); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolverTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolverTests.java index 8f7d46f30..53dbac01a 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolverTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ArgumentMethodArgumentResolverTests.java @@ -86,6 +86,18 @@ void shouldResolveJavaBeanArgument() throws Exception { .hasFieldOrPropertyWithValue("authorId", 42L); } + @Test + void shouldResolveKotlinBeanArgument() throws Exception { + Method addBook = ClassUtils.getMethod(BookController.class, "ktAddBook", KotlinBookInput.class); + String payload = "{\"bookInput\": { \"name\": \"test name\", \"authorId\": 42} }"; + DataFetchingEnvironment environment = initEnvironment(payload); + MethodParameter methodParameter = getMethodParameter(addBook, 0); + Object result = resolver.resolveArgument(methodParameter, environment); + assertThat(result).isNotNull().isInstanceOf(KotlinBookInput.class); + assertThat((KotlinBookInput) result).hasFieldOrPropertyWithValue("name", "test name") + .hasFieldOrPropertyWithValue("authorId", 42L); + } + @Test void shouldResolveListOfJavaBeansArgument() throws Exception { Method addBooks = ClassUtils.getMethod(BookController.class, "addBooks", List.class); @@ -147,6 +159,11 @@ public Book addBook(@Argument BookInput bookInput) { return null; } + @MutationMapping + public Book ktAddBook(@Argument KotlinBookInput bookInput) { + return null; + } + @MutationMapping public List addBooks(@Argument List books) { return null; diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/KotlinBookInput.kt b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/KotlinBookInput.kt new file mode 100644 index 000000000..356db4f81 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/KotlinBookInput.kt @@ -0,0 +1,3 @@ +package org.springframework.graphql.data.method.annotation.support; + +data class KotlinBookInput(val name: String, val authorId: Long)