From dfeb64278099324147c5be16d1c84ed35467023e Mon Sep 17 00:00:00 2001 From: reginaldalfret Date: Sun, 25 Oct 2026 20:06:59 +0530 Subject: [PATCH] Handle object literal default values on custom scalar input fields When an input field type is a custom scalar with an object literal default value (e.g. metadata: JSON = {}), the default value generators in DataTypeGenerator (Java) and GenerateKotlinCode (Kotlin) previously assumed the field type was an input object and invoked inputTypeDefinitions.first { ... }, throwing NoSuchElementException because no input type definition matches. Additionally, JavaTypeName.className and TypeName.className assumed ParameterizedTypeName was always a List and accessed the first type argument, which returned the key type rather than raw Map when mapped to a Map. This change: 1. Uses firstOrNull in DataTypeGenerator and GenerateKotlinCode to gracefully handle non-input-object types, emitting emptyMap() or fallback initializers for Map scalars. 2. Updates JavaTypeName.className and TypeName.className to preserve rawType for non-List parameterized types like Map. 3. Uses cross-platform File.pathSeparator in TestUtils.classpath(). 4. Adds regression tests for both Java and Kotlin code generation. Signed-off-by: reginaldalfret --- .../generators/java/DataTypeGenerator.kt | 24 ++++++++++-- .../generators/shared/GenerateKotlinCode.kt | 38 ++++++++++++++++++- .../graphql/dgs/codegen/CodeGenTest.kt | 23 +++++++++++ .../graphql/dgs/codegen/KotlinCodeGenTest.kt | 25 ++++++++++++ .../netflix/graphql/dgs/codegen/TestUtils.kt | 3 +- 5 files changed, 107 insertions(+), 6 deletions(-) diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt index b3cb70784..ef82cd337 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/DataTypeGenerator.kt @@ -285,12 +285,25 @@ class InputTypeGenerator internal constructor( is ObjectValue -> { val inputObjectDefinition = - inputTypeDefinitions.first { + inputTypeDefinitions.firstOrNull { val expectedCanonicalClassName = config.typeMapping[it.name] ?: "${config.packageNameTypes}.${it.name}" expectedCanonicalClassName == type.className.canonicalName() } - if (value.objectFields.isEmpty()) { + if (inputObjectDefinition == null) { + val canonical = type.className.canonicalName() + if (canonical == "java.util.Map" || canonical == "java.util.HashMap") { + if (value.objectFields.isEmpty()) { + CodeBlock.of("\$T.emptyMap()", Collections::class.java) + } else { + null + } + } else if (value.objectFields.isEmpty()) { + CodeBlock.of("new \$T()", type) + } else { + null + } + } else if (value.objectFields.isEmpty()) { CodeBlock.of("new \$T()", type) } else { CodeBlock.of( @@ -379,7 +392,12 @@ class InputTypeGenerator internal constructor( get() = when (this) { is ClassName -> this - is ParameterizedTypeName -> typeArguments().first().className + is ParameterizedTypeName -> + if (rawType().canonicalName() == "java.util.List") { + typeArguments().first().className + } else { + rawType() + } else -> throw UnsupportedOperationException("Unknown type: ${this.javaClass}") } } diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/shared/GenerateKotlinCode.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/shared/GenerateKotlinCode.kt index 7811d3666..4d07424bf 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/shared/GenerateKotlinCode.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/shared/GenerateKotlinCode.kt @@ -61,12 +61,40 @@ fun generateKotlinCode( is ObjectValue -> { val inputObjectDefinition = - inputTypeDefinitions.first { + inputTypeDefinitions.firstOrNull { val expectedCanonicalClassName = config.typeMapping[it.name] ?: "${config.packageNameTypes}.${it.name}" expectedCanonicalClassName == type.className.canonicalName } + if (inputObjectDefinition == null) { + val canonical = type.className.canonicalName + if (canonical == "kotlin.collections.Map" || canonical == "java.util.Map") { + if (value.objectFields.isEmpty()) { + CodeBlock.of("emptyMap()") + } else { + CodeBlock.of( + "mapOf(%L)", + value.objectFields.joinToString { objectProperty -> + "\"${objectProperty.name}\" to ${ + generateKotlinCode( + objectProperty.value, + type, + inputTypeDefinitions, + config, + typeUtils, + ) + }" + }, + ) + } + } else if (value.objectFields.isEmpty()) { + CodeBlock.of("%T()", type) + } else { + CodeBlock.of("null") + } + } else { + CodeBlock.of( type.className.canonicalName + "(%L)", value.objectFields.joinToString { objectProperty -> @@ -85,6 +113,7 @@ fun generateKotlinCode( }" }, ) + } } is NullValue -> CodeBlock.of("null") @@ -147,6 +176,11 @@ private val TypeName.className: ClassName get() = when (this) { is ClassName -> this - is ParameterizedTypeName -> typeArguments[0].className + is ParameterizedTypeName -> + if (rawType.canonicalName == "kotlin.collections.List" || rawType.canonicalName == "java.util.List") { + typeArguments[0].className + } else { + rawType + } else -> TODO() } diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt index a001390ce..18b093797 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/CodeGenTest.kt @@ -2123,6 +2123,29 @@ class CodeGenTest { assertCompilesJava(codeGenResult) } + @Test + fun `Input field with scalar type and object literal default value does not fail in Java`() { + val schema = + """ + scalar JSON + type Query { + search(filter: FilterInput): String + } + input FilterInput { + metadata: JSON = {} + } + """.trimIndent() + val codeGenResult = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + typeMapping = mapOf("JSON" to "java.util.Map"), + ), + ).generate() + assertCompilesJava(codeGenResult) + } + @ParameterizedTest @ValueSource(booleans = [true, false]) fun generateInputWithDefaultValueForEnum(trackInputFieldSet: Boolean) { diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt index 408c7e73c..06fee54df 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/KotlinCodeGenTest.kt @@ -75,6 +75,30 @@ class KotlinCodeGenTest { assertCompilesKotlin(codeGenResult) } + @Test + fun `Input field with scalar type and object literal default value does not fail in Kotlin`() { + val schema = + """ + scalar JSON + type Query { + search(filter: FilterInput): String + } + input FilterInput { + metadata: JSON = {} + } + """.trimIndent() + val codeGenResult = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + language = Language.KOTLIN, + typeMapping = mapOf("JSON" to "Map"), + ), + ).generate() + assertCompilesKotlin(codeGenResult) + } + @Test fun generateDataClassesOutsideMetaInf() { val schema = @@ -5366,3 +5390,4 @@ It takes a title and such. } """.trimIndent() } + diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/TestUtils.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/TestUtils.kt index e947607c5..7c5bc4d33 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/TestUtils.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/TestUtils.kt @@ -113,7 +113,7 @@ fun classpath(): String = .split(System.getProperty("path.separator")) .filter { File(it).exists() && File(it).canRead() - }.joinToString(":") + }.joinToString(File.pathSeparator) fun codegenTestClassLoader( compilation: Compilation, @@ -202,3 +202,4 @@ fun KAnnotationSpec.canonicalName() = (typeName as KClassName).canonicalName const val BASE_PACKAGE_NAME = "com.netflix.graphql.dgs.codegen.tests.generated" const val TYPES_PACKAGE_NAME = "$BASE_PACKAGE_NAME.types" const val DATA_FETCHER_PACKAGE_NAME = "$BASE_PACKAGE_NAME.datafetchers" +