diff --git a/src/main/kotlin/graphql/kickstart/tools/GenericType.kt b/src/main/kotlin/graphql/kickstart/tools/GenericType.kt index 820ab845..5d681f27 100644 --- a/src/main/kotlin/graphql/kickstart/tools/GenericType.kt +++ b/src/main/kotlin/graphql/kickstart/tools/GenericType.kt @@ -151,8 +151,13 @@ internal open class GenericType(protected val mostSpecificType: JavaType, protec } } is TypeVariable<*> -> { - if (declaringType is ParameterizedType) { - TypeUtils.getRawType(type, declaringType) + val genericDeclaration = type.genericDeclaration + if (declaringType is ParameterizedType && genericDeclaration is Class<*>) { + // keep the full type argument (e.g. List) rather than its raw class so nested generics aren't lost + TypeUtils.getTypeArguments(declaringType, genericDeclaration)?.get(type) + ?.takeIf { it != type } + ?.let { replaceTypeVariable(it) } + ?: type } else { type } diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index e91e3e2c..ccfc23ae 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -368,7 +368,7 @@ internal class SchemaClassScanner( handleFoundType(typeClassMatcher.match(TypeClassMatcher.PotentialMatch.parameterType( inputValueDefinition.type, inputValueJavaType, - GenericType(javaType, options).relativeToType(inputValueJavaType), + GenericType(javaType, options).relativeToType(javaType), InputObjectReference(inputValueDefinition) ))) } else { diff --git a/src/test/kotlin/graphql/kickstart/tools/GenericInputTypesTest.kt b/src/test/kotlin/graphql/kickstart/tools/GenericInputTypesTest.kt new file mode 100644 index 00000000..f58752f3 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/GenericInputTypesTest.kt @@ -0,0 +1,103 @@ +package graphql.kickstart.tools + +import graphql.GraphQL +import org.junit.Test + +class GenericInputTypesTest { + + @Test + fun `generic input types are parsed`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + audit(input: LanguageAudit!): String! + audits(input: LanguageAudits!): String! + } + + input LanguageAudit { + content: LanguageInput! + operator: String! + } + + input LanguageAudits { + content: [LanguageInput!]! + operator: String! + } + + input LanguageInput { + id: ID! + } + """) + .resolvers(QueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + audit(input: { content: { id: "1" }, operator: "op" }) + audits(input: { content: [{ id: "2" }, { id: "3" }], operator: "op" }) + } + """ + } + + assertEquals(data["audit"], "op:1") + assertEquals(data["audits"], "op:2,3") + } + + @Test + fun `generic input types inherited from a parameterized superclass are parsed`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + audit(input: LanguageAudit!): String! + } + + input LanguageAudit { + content: LanguageInput! + operator: String! + } + + input LanguageInput { + id: ID! + } + """) + .resolvers(InheritedQueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + audit(input: { content: { id: "1" }, operator: "op" }) + } + """ + } + + assertEquals(data["audit"], "op:1") + } + + class QueryResolver : GraphQLQueryResolver { + fun audit(input: AuditWrapper): String = "${input.operator}:${input.content?.id}" + fun audits(input: AuditWrapper>): String = "${input.operator}:${input.content?.joinToString(",") { it.id.orEmpty() }}" + } + + class InheritedQueryResolver : GraphQLQueryResolver { + fun audit(input: LanguageAuditWrapper): String = "${input.operator}:${input.content?.id}" + } + + open class AuditWrapper { + var content: T? = null + var operator: String? = null + } + + class LanguageAuditWrapper : AuditWrapper() + + class LanguageInput { + var id: String? = null + } +}