From d050dd793b0b57e9ca340eb67fae8c594750be24 Mon Sep 17 00:00:00 2001 From: Oryan Date: Sat, 3 Oct 2026 18:16:33 -0400 Subject: [PATCH] Prefer getters over fluent setters for input field types To find the Java type of an input object field, the scanner took any public method named `x` or `getX`, picked the one with the shortest name and used its return type. A fluent setter such as `RepairApply repairMan(RepairMan)`, as generated by JHipster or Lombok @Accessors(fluent = true), has the shortest name, so the input type of the field was mapped to the enclosing class. That fails with "Two different classes used for type" as soon as the same input type is also used as a resolver parameter. Look for a getter without parameters first, then for a public field. Methods with parameters, such as `getX(DataFetchingEnvironment)`, are only used when neither exists, so nested input types that can only be found through them are still discovered. Fixes #446 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../kickstart/tools/SchemaClassScanner.kt | 10 +- .../kickstart/tools/SchemaClassScannerTest.kt | 116 ++++++++++++++++++ 2 files changed, 122 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index d2215367..6c3fbce1 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -436,13 +436,15 @@ internal class SchemaClassScanner( private fun findInputValueTypeInType(name: String, clazz: Class<*>): JavaType? { val methods = clazz.methods - val filteredMethods = methods.filter { + val (getters, methodsWithParameters) = methods.filter { it.name == name || it.name == "get${name.replaceFirstChar(Char::titlecase)}" - }.sortedBy { it.name.length } + }.sortedBy { it.name.length }.partition { it.parameterCount == 0 } - return filteredMethods.find { !it.isSynthetic }?.genericReturnType - ?: filteredMethods.firstOrNull()?.genericReturnType + return getters.find { !it.isSynthetic }?.genericReturnType + ?: getters.firstOrNull()?.genericReturnType ?: clazz.fields.find { it.name == name }?.genericType + ?: methodsWithParameters.find { !it.isSynthetic }?.genericReturnType + ?: methodsWithParameters.firstOrNull()?.genericReturnType } private data class QueueItem(val type: ObjectTypeDefinition, val clazz: JavaType) diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt index 67671ab5..24f0e662 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt @@ -146,6 +146,122 @@ class SchemaClassScannerTest { } } + @Test + fun `scanner ignores fluent setters when finding input field types`() { + SchemaParser.newParser() + .resolvers(FluentSetterMutation(), object : GraphQLQueryResolver { + fun test(): Boolean = true + }) + .schemaString( + """ + type Query { + test: Boolean + } + + type Mutation { + createRepairApply(body: RepairApplyInput): Boolean + createRepairMan(body: RepairManInput): Boolean + } + + input RepairApplyInput { + id: ID + repairMan: RepairManInput + reviewer: RepairManInput + approver: RepairManInput + } + + input RepairManInput { + id: ID + userName: String + } + """) + .build() + .makeExecutableSchema() + } + + private class FluentSetterMutation : GraphQLMutationResolver { + fun createRepairApply(body: RepairApply): Boolean = true + fun createRepairMan(body: RepairMan): Boolean = true + + class RepairApply { + var id: String? = null + var repairMan: RepairMan? = null + private var reviewer: RepairMan? = null + @JvmField + var approver: RepairMan? = null + + fun repairMan(repairMan: RepairMan?): RepairApply { + this.repairMan = repairMan + return this + } + + fun reviewer(): RepairMan? = reviewer + + fun reviewer(reviewer: RepairMan?): RepairApply { + this.reviewer = reviewer + return this + } + + fun approver(approver: RepairMan?): RepairApply { + this.approver = approver + return this + } + } + + class RepairMan { + var id: String? = null + var userName: String? = null + } + } + + @Test + fun `scanner finds input field types through getters with arguments`() { + SchemaParser.newParser() + .resolvers(GetterWithArgumentsQuery()) + .schemaString( + """ + type Query { + foo(input: FooInput): Foo + } + + type Foo { + bar: Bar + } + + type Bar { + name: String + } + + input FooInput { + bar: BarInput + } + + input BarInput { + name: String + } + """) + .build() + .makeExecutableSchema() + } + + private class GetterWithArgumentsQuery : GraphQLQueryResolver { + fun foo(input: Foo): Foo = input + + class Foo { + private var bar: Bar? = null + + fun getBar(env: DataFetchingEnvironment): Bar? = bar + + fun setBar(bar: Bar?) { + this.bar = bar + } + } + + class Bar { + var name: String? = null + } + } + @Test fun `scanner handles input types extensions`() { val schema = SchemaParser.newParser()