diff --git a/graphql-dgs/src/main/kotlin/com/netflix/graphql/dgs/internal/DefaultInputObjectMapper.kt b/graphql-dgs/src/main/kotlin/com/netflix/graphql/dgs/internal/DefaultInputObjectMapper.kt index aca0518e4..3cc453255 100644 --- a/graphql-dgs/src/main/kotlin/com/netflix/graphql/dgs/internal/DefaultInputObjectMapper.kt +++ b/graphql-dgs/src/main/kotlin/com/netflix/graphql/dgs/internal/DefaultInputObjectMapper.kt @@ -31,6 +31,7 @@ import org.springframework.core.convert.support.DefaultConversionService import org.springframework.util.CollectionUtils import org.springframework.util.ReflectionUtils import java.lang.reflect.Type +import java.util.Optional import kotlin.reflect.KClass import kotlin.reflect.KParameter import kotlin.reflect.full.primaryConstructor @@ -54,6 +55,10 @@ class DefaultInputObjectMapper(customInputObjectMapper: InputObjectMapper? = nul } override fun matches(sourceType: TypeDescriptor, targetType: TypeDescriptor): Boolean { + if (targetType.type == Optional::class.java) { + // Let Spring's ObjectToOptionalConverter handle it + return false + } if (sourceType.isMap) { val keyDescriptor = sourceType.mapKeyTypeDescriptor return keyDescriptor == null || keyDescriptor.type == String::class.java diff --git a/graphql-dgs/src/test/java/com/netflix/graphql/dgs/internal/java/test/inputobjects/JInputObjectWithOptional.java b/graphql-dgs/src/test/java/com/netflix/graphql/dgs/internal/java/test/inputobjects/JInputObjectWithOptional.java new file mode 100644 index 000000000..f0d509ca9 --- /dev/null +++ b/graphql-dgs/src/test/java/com/netflix/graphql/dgs/internal/java/test/inputobjects/JInputObjectWithOptional.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.netflix.graphql.dgs.internal.java.test.inputobjects; + +import java.util.Optional; + +public class JInputObjectWithOptional { + private Optional foo = Optional.empty(); + private Optional bar = Optional.empty(); + + public JInputObjectWithOptional() {} + + public Optional getFoo() { + return foo; + } + + public void setFoo(Optional foo) { + this.foo = foo; + } + + public Optional getBar() { + return bar; + } + + public void setBar(Optional bar) { + this.bar = bar; + } +} diff --git a/graphql-dgs/src/test/kotlin/com/netflix/graphql/dgs/internal/InputObjectMapperTest.kt b/graphql-dgs/src/test/kotlin/com/netflix/graphql/dgs/internal/InputObjectMapperTest.kt index 184cee970..3a2e2c4b5 100644 --- a/graphql-dgs/src/test/kotlin/com/netflix/graphql/dgs/internal/InputObjectMapperTest.kt +++ b/graphql-dgs/src/test/kotlin/com/netflix/graphql/dgs/internal/InputObjectMapperTest.kt @@ -22,11 +22,14 @@ import com.netflix.graphql.dgs.internal.java.test.inputobjects.JGenericSubInputO import com.netflix.graphql.dgs.internal.java.test.inputobjects.JInputObject import com.netflix.graphql.dgs.internal.java.test.inputobjects.JInputObjectWithKotlinProperty import com.netflix.graphql.dgs.internal.java.test.inputobjects.JInputObjectWithMap +import com.netflix.graphql.dgs.internal.java.test.inputobjects.JInputObjectWithOptional import com.netflix.graphql.dgs.internal.java.test.inputobjects.JInputObjectWithSet +import org.assertj.core.api.Assertions.COLLECTION import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test import java.time.LocalDateTime +import java.util.Optional import kotlin.reflect.KClass internal class InputObjectMapperTest { @@ -247,6 +250,28 @@ internal class InputObjectMapperTest { assertThat(result.items).isEqualTo(setOf(1, 2, 3, 4)) } + @Test + fun `mapping to an object with Optional fields works`() { + var result = inputObjectMapper.mapToKotlinObject(mapOf("foo" to null, "bar" to mapOf("subkey1" to "subkey1-value")), InputWithOptional::class) + assertThat(result.foo).isNotPresent + assertThat(result.bar).get().isEqualTo(KotlinSubObject("subkey1-value")) + + result = inputObjectMapper.mapToKotlinObject(mapOf("foo" to "foo-value", "bar" to null), InputWithOptional::class) + assertThat(result.foo).get().isEqualTo("foo-value") + assertThat(result.bar).isNotPresent + } + + @Test + fun `mapping to a Java object with Optional fields works`() { + var result = inputObjectMapper.mapToJavaObject(mapOf("foo" to null, "bar" to mapOf("items" to listOf(1, 2, 3, 4))), JInputObjectWithOptional::class.java) + assertThat(result.foo).isNotPresent + assertThat(result.bar).get().extracting("items").asInstanceOf(COLLECTION).containsExactly(1, 2, 3, 4) + + result = inputObjectMapper.mapToJavaObject(mapOf("foo" to "foo-value", "bar" to null), JInputObjectWithOptional::class.java) + assertThat(result.foo).get().isEqualTo("foo-value") + assertThat(result.bar).isNotPresent + } + data class KotlinInputObject(val simpleString: String?, val someDate: LocalDateTime, val someObject: KotlinSomeObject) data class KotlinNestedInputObject(val input: KotlinInputObject) data class KotlinDoubleNestedInputObject(val inputL1: KotlinNestedInputObject) @@ -259,4 +284,6 @@ internal class InputObjectMapperTest { enum class FieldType { FOO, BAR, BAZ } data class KotlinObjectWithEnumField(val name: String, val type: FieldType) + + data class InputWithOptional(val foo: Optional, val bar: Optional) }