From c8a7fb909c31dd64987135f50056ec28a69a5764 Mon Sep 17 00:00:00 2001 From: Oryan Date: Sat, 3 Oct 2026 18:17:22 -0400 Subject: [PATCH 1/2] Allow list fields on property map types Map-backed types could not declare list fields. MapFieldResolver resolves the value type of an untyped map to Object, and the ListType branch of TypeClassMatcher only accepted a parameterized Iterable or an array, so schema building failed with "Java class is not a List or generic type information was lost: class java.lang.Object". Object already matches scalar and object type fields, so a list field backed by Object now matches its element type as Object too. Scalar elements produce a scalar match and object type elements go through the existing dictionary lookup for Object, which still reports a missing dictionary entry. Maps with a typed list value type keep matching through the parameterized Iterable branch. Fixes #295 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../kickstart/tools/TypeClassMatcher.kt | 3 + .../kickstart/tools/PropertyMapListTest.kt | 129 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/test/kotlin/graphql/kickstart/tools/PropertyMapListTest.kt diff --git a/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt b/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt index 324003f2..fe24c982 100644 --- a/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt +++ b/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt @@ -70,6 +70,9 @@ internal class TypeClassMatcher(private val definitionsByName: Map && realType.isArray) { match(potentialMatch, graphQLType.type, realType.componentType) + } else if (realType == Object::class.java) { + // e.g. a property map value: the element type is unknown, so match it as Object too + match(potentialMatch, graphQLType.type, realType) } else { throw error(potentialMatch, "Java class is not a List or generic type information was lost: $realType") } diff --git a/src/test/kotlin/graphql/kickstart/tools/PropertyMapListTest.kt b/src/test/kotlin/graphql/kickstart/tools/PropertyMapListTest.kt new file mode 100644 index 00000000..936c7c82 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/PropertyMapListTest.kt @@ -0,0 +1,129 @@ +package graphql.kickstart.tools + +import graphql.GraphQL +import org.junit.Assert.assertThrows +import org.junit.Test + +class PropertyMapListTest { + + @Test + fun `property maps should support list fields`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + greeting: Greeting! + } + + type Greeting { + name: String! + tags: [String!]! + matrix: [[Int]] + key: Key + keys: [Key!] + } + + type Key { + a: String + b: [String] + } + """) + .resolvers(object : GraphQLQueryResolver { + fun greeting(): Map = mapOf( + "name" to "hello", + "tags" to listOf("x", "y"), + "matrix" to listOf(listOf(1, 2), listOf(3)), + "key" to hashMapOf("a" to "one", "b" to listOf("ss")), + "keys" to listOf(hashMapOf("a" to "two", "b" to listOf("tt"))) + ) + }) + .dictionary("Key", HashMap::class) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + { + greeting { + name + tags + matrix + key { a b } + keys { a b } + } + } + """ + } + + assertEquals(data["greeting"], mapOf( + "name" to "hello", + "tags" to listOf("x", "y"), + "matrix" to listOf(listOf(1, 2), listOf(3)), + "key" to mapOf("a" to "one", "b" to listOf("ss")), + "keys" to listOf(mapOf("a" to "two", "b" to listOf("tt"))) + )) + } + + @Test + fun `property maps with typed list values should support list fields`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + tags: Tags! + } + + type Tags { + names: [String!]! + } + """) + .resolvers(object : GraphQLQueryResolver { + fun tags(): TagsMap = TagsMap().apply { put("names", listOf("x", "y")) } + }) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + { + tags { + names + } + } + """ + } + + assertEquals(data["tags"], mapOf("names" to listOf("x", "y"))) + } + + @Test + fun `property maps with object list fields should require a dictionary entry`() { + val error = assertThrows(SchemaClassScannerError::class.java) { + SchemaParser.newParser() + .schemaString( + """ + type Query { + greeting: Greeting! + } + + type Greeting { + keys: [Key!] + } + + type Key { + a: String + } + """) + .resolvers(object : GraphQLQueryResolver { + fun greeting(): Map = mapOf() + }) + .build() + } + + assertEquals(error.message, "The GraphQL schema type 'Key' maps to a field of type java.lang.Object however there is no matching entry for this type in the type dictionary. You may need to add this type to the dictionary before building the schema.") + } + + class TagsMap : HashMap>() +} From 652c6302f40f7de2eacc7e0d672bd194fa1a9136 Mon Sep 17 00:00:00 2001 From: Oryan Date: Sat, 3 Oct 2026 18:47:58 -0400 Subject: [PATCH 2/2] Use when for list type matching Co-Authored-By: Claude Opus 5.5 (1M context) --- .../graphql/kickstart/tools/TypeClassMatcher.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt b/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt index fe24c982..b505b16d 100644 --- a/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt +++ b/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt @@ -65,17 +65,16 @@ internal class TypeClassMatcher(private val definitionsByName: Map { - if (realType is ParameterizedType && isListType(realType, potentialMatch)) { + is ListType -> when { + realType is ParameterizedType && isListType(realType, potentialMatch) -> match(potentialMatch, graphQLType.type, realType.actualTypeArguments.first()) - } else if (realType is Class<*> && realType.isArray) { + realType is Class<*> && realType.isArray -> match(potentialMatch, graphQLType.type, realType.componentType) - } else if (realType == Object::class.java) { - // e.g. a property map value: the element type is unknown, so match it as Object too + // e.g. a property map value: the element type is unknown, so match it as Object too + realType == Object::class.java -> match(potentialMatch, graphQLType.type, realType) - } else { + else -> throw error(potentialMatch, "Java class is not a List or generic type information was lost: $realType") - } } is TypeName -> {