From 04ccd9c4bbc96efa95c3632667c6b2c2044eacd5 Mon Sep 17 00:00:00 2001 From: Oryan Date: Mon, 12 Oct 2026 15:23:34 -0400 Subject: [PATCH 1/4] Let input types share a dictionary class SchemaParserDictionary stored its entries in a BiMap, so mapping two type names to one class, e.g. two input types to ObjectNode or Map, threw "value already present" as soon as the second was added. Input types never go into the scanner's type => class bimap, so nothing after that needs their classes to be unique. Directive input types still need a dictionary entry, and entries for nested inputs under a Map or ObjectNode are a common workaround, so both hit this. The dictionary is now a plain map. Every other kind of type still needs a class of its own, but the scanner checks that when the schema is built and names the class and the types sharing it. getDictionary() keeps its signature, since graphql-spring-boot passes its result to the builder. If several types share a class, the inverse of the returned copy only maps it back to the first of them. Names other than input types that share a class now fail in build() with a SchemaClassScannerError, instead of in dictionary() with an IllegalArgumentException. Co-Authored-By: Claude Opus 5.5 --- .../kickstart/tools/SchemaClassScanner.kt | 13 +- .../kickstart/tools/SchemaParserDictionary.kt | 9 +- .../graphql/kickstart/tools/util/BiMap.java | 12 + .../tools/SchemaParserDictionaryTest.kt | 278 ++++++++++++++++++ .../graphql/kickstart/tools/util/BiMapTest.kt | 15 + 5 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index 585a7092..91cd8ed3 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -16,7 +16,7 @@ import java.lang.reflect.Method * @author Andrew Potter */ internal class SchemaClassScanner( - initialDictionary: BiMap>, + initialDictionary: Map>, allDefinitions: List>, resolvers: List>, private val scalars: CustomScalarMap, @@ -59,6 +59,17 @@ internal class SchemaClassScanner( } } + // Input types never go into the type => class bimap, so several of them can share a class, e.g. ObjectNode. + // Other types need one each. + initialDictionary + .filterKeys { definitionsByName[it] !is InputObjectTypeDefinition } + .entries.groupBy({ it.value }, { it.key }) + .filterValues { it.size > 1 } + .entries.firstOrNull() + ?.let { (clazz, names) -> + throw SchemaClassScannerError("Class in supplied dictionary '${clazz.name}' specified type names ${names.joinToString { "'$it'" }}, but only input object types can share a class!") + } + if (options.allowUnimplementedResolvers) { log.warn("Option 'allowUnimplementedResolvers' should only be set to true during development, as it can cause schema errors to be moved to query time instead of schema creation time. Make sure this is turned off in production.") } diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaParserDictionary.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaParserDictionary.kt index 116fa7ca..1fc10eeb 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaParserDictionary.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaParserDictionary.kt @@ -5,9 +5,14 @@ import kotlin.reflect.KClass class SchemaParserDictionary { - private val dictionary: BiMap> = BiMap.create() + // Not a BiMap, since several input types can share a class, e.g. ObjectNode. The scanner checks that other types don't. + private val dictionary = linkedMapOf>() - fun getDictionary(): BiMap> = BiMap.unmodifiableBiMap(dictionary) + /** + * Returns a copy of the entries added so far. If several types share a class, the inverse of the copy only maps that class + * back to the first of them. + */ + fun getDictionary(): BiMap> = BiMap.unmodifiableCopyOf(dictionary) /** * Add arbitrary classes to the parser's dictionary, overriding the generated type name. diff --git a/src/main/kotlin/graphql/kickstart/tools/util/BiMap.java b/src/main/kotlin/graphql/kickstart/tools/util/BiMap.java index 230f995c..e1ec9603 100644 --- a/src/main/kotlin/graphql/kickstart/tools/util/BiMap.java +++ b/src/main/kotlin/graphql/kickstart/tools/util/BiMap.java @@ -1,6 +1,7 @@ package graphql.kickstart.tools.util; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -21,6 +22,17 @@ public static BiMap unmodifiableBiMap(BiMap biMap) { return new BiMap<>(unmodifiableMap(biMap.delegate), unmodifiableMap(biMap.inverse)); } + /** + * Returns an unmodifiable bimap with the entries of the given map. Unlike {@link #put(Object, Object)}, several keys may + * have the same value. The inverse only maps such a value back to the first of its keys, so the inverse and + * {@link #values()} can have fewer entries than the bimap. + */ + public static BiMap unmodifiableCopyOf(Map map) { + Map inverse = new HashMap<>(); + map.forEach((key, value) -> inverse.putIfAbsent(value, key)); + return new BiMap<>(unmodifiableMap(new LinkedHashMap<>(map)), unmodifiableMap(inverse)); + } + private BiMap(Map delegate, Map inverse) { this.delegate = delegate; this.inverse = inverse; diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt new file mode 100644 index 00000000..9629e9c3 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt @@ -0,0 +1,278 @@ +package graphql.kickstart.tools + +import com.fasterxml.jackson.databind.node.ObjectNode +import graphql.GraphQL +import org.junit.Assert.assertThrows +import org.junit.Test + +class SchemaParserDictionaryTest { + + private val entitySchema = """ + type Query { + test: String + } + + type Mutation { + create(input: EntityCreateInput!): String + update(input: EntityUpdateInput!): String + } + + input EntityCreateInput { + name: String + nested: Nested + } + + input EntityUpdateInput { + id: ID! + nested: Nested + } + + input Nested { + x: Int + } + """ + + private val directiveSchema = """ + directive @a(arg: AInput) on FIELD_DEFINITION + directive @b(arg: BInput) on FIELD_DEFINITION + + type Query { + test: String @a(arg: { x: 1 }) @b(arg: { y: "b" }) + } + + input AInput { + x: Int + } + + input BInput { + y: String + } + """ + + @Test + fun `input types can share a dictionary class`() { + val schema = SchemaParser.newParser() + .schemaString(entitySchema) + .resolvers(EntityQuery(), EntityMutation()) + .dictionary("EntityCreateInput", ObjectNode::class.java) + .dictionary("EntityUpdateInput", ObjectNode::class.java) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + mutation { + create(input: { name: "n", nested: { x: 1 } }) + update(input: { id: "2", nested: { x: 3 } }) + } + """ + } + + assertEquals(data["create"], """{"name":"n","nested":{"x":1}}""") + assertEquals(data["update"], """{"id":"2","nested":{"x":3}}""") + } + + @Test + fun `nested input types can share a dictionary class`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + create(input: EntityInput!): String + } + + input EntityInput { + name: String + a: NestedA + b: [NestedB!] + } + + input NestedA { + x: Int + } + + input NestedB { + y: Int + } + """) + .resolvers(object : GraphQLQueryResolver { + fun create(input: ObjectNode): String = input.toString() + }) + .dictionary("NestedA", ObjectNode::class.java) + .dictionary("NestedB", ObjectNode::class.java) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + create(input: { name: "n", a: { x: 1 }, b: [{ y: 2 }] }) + } + """ + } + + assertEquals(data["create"], """{"name":"n","a":{"x":1},"b":[{"y":2}]}""") + } + + @Test + fun `directive input types can share a dictionary class`() { + val schema = SchemaParser.newParser() + .schemaString(directiveSchema) + .resolvers(DirectiveQuery()) + .dictionary("AInput", Map::class.java) + .dictionary("BInput", Map::class.java) + .build() + .makeExecutableSchema() + + val field = schema.queryType.getFieldDefinition("test") + assertEquals(field.getAppliedDirective("a").getArgument("arg")!!.getValue>(), mapOf("x" to 1)) + assertEquals(field.getAppliedDirective("b").getArgument("arg")!!.getValue>(), mapOf("y" to "b")) + + val gql = GraphQL.newGraphQL(schema).build() + val data = assertNoGraphQlErrors(gql) { + """ + query { + test + } + """ + } + + assertEquals(data["test"], "test") + } + + @Test + fun `input types sharing a class can be passed to the builder from a SchemaParserDictionary`() { + val dictionary = SchemaParserDictionary() + .add("AInput", Map::class) + .add("BInput", Map::class) + + assertEquals(dictionary.getDictionary().toMap(), mapOf("AInput" to Map::class.java, "BInput" to Map::class.java)) + + // graphql-spring-boot reads a SchemaParserDictionary bean this way + val schema = SchemaParser.newParser() + .schemaString(directiveSchema) + .resolvers(DirectiveQuery()) + .dictionary(dictionary.getDictionary()) + .build() + .makeExecutableSchema() + + val field = schema.queryType.getFieldDefinition("test") + assertEquals(field.getAppliedDirective("b").getArgument("arg")!!.getValue>(), mapOf("y" to "b")) + } + + @Test + fun `input type and object type can share a dictionary class`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + node: Node + } + + type Mutation { + save(item: ItemInput!): Node + } + + interface Node { + name: String + } + + type Item implements Node { + name: String + } + + input ItemInput { + name: String + } + """) + .resolvers(object : GraphQLQueryResolver { + fun node(): Node = Item().apply { name = "a" } + }, object : GraphQLMutationResolver { + fun save(item: Item): Node = item + }) + .dictionary("Item", Item::class.java) + .dictionary("ItemInput", Item::class.java) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema).build() + + val queryData = assertNoGraphQlErrors(gql) { + """ + query { + node { + __typename + name + } + } + """ + } + val mutationData = assertNoGraphQlErrors(gql) { + """ + mutation { + save(item: { name: "b" }) { + __typename + name + } + } + """ + } + + assertEquals(queryData["node"], mapOf("__typename" to "Item", "name" to "a")) + assertEquals(mutationData["save"], mapOf("__typename" to "Item", "name" to "b")) + } + + @Test + fun `object types can't share a dictionary class`() { + val error = assertThrows(SchemaClassScannerError::class.java) { + SchemaParser.newParser() + .schemaString( + """ + type Query { + a: A + b: B + } + + type A { + name: String + } + + type B { + name: String + } + """) + .resolvers(object : GraphQLQueryResolver { + fun a(): Item? = null + fun b(): Item? = null + }) + .dictionary("A", Item::class.java) + .dictionary("B", Item::class.java) + .build() + } + + assertEquals(error.message, "Class in supplied dictionary '${Item::class.java.name}' specified type names 'A', 'B', " + + "but only input object types can share a class!") + } + + class EntityQuery : GraphQLQueryResolver { + fun test(): String? = null + } + + class EntityMutation : GraphQLMutationResolver { + fun create(input: ObjectNode): String = input.toString() + fun update(input: ObjectNode): String = input.toString() + } + + class DirectiveQuery : GraphQLQueryResolver { + fun test(): String = "test" + } + + interface Node { + val name: String? + } + + class Item : Node { + override var name: String? = null + } +} diff --git a/src/test/kotlin/graphql/kickstart/tools/util/BiMapTest.kt b/src/test/kotlin/graphql/kickstart/tools/util/BiMapTest.kt index fc753990..fdc56f30 100644 --- a/src/test/kotlin/graphql/kickstart/tools/util/BiMapTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/util/BiMapTest.kt @@ -67,4 +67,19 @@ class BiMapTest { val expected = setOf(1, 2, 3) assertEquals(expected, values) } + + @Test + fun `unmodifiable copy should keep keys with the same value`() { + val copy = BiMap.unmodifiableCopyOf(linkedMapOf("foo" to 1, "bar" to 2, "baz" to 1)) + + assertEquals(mapOf("foo" to 1, "bar" to 2, "baz" to 1), copy.toMap()) + assertEquals(listOf("foo", "bar", "baz"), copy.keys.toList()) + assertEquals(setOf(1, 2), copy.values) + assertEquals(mapOf(1 to "foo", 2 to "bar"), copy.inverse().toMap()) + } + + @Test(expected = UnsupportedOperationException::class) + fun `unmodifiable copy should not permit changes`() { + BiMap.unmodifiableCopyOf(mapOf("foo" to 1))["bar"] = 2 + } } From 5a8aa0543f7018ff3a4143495e07a44d0febef3d Mon Sep 17 00:00:00 2001 From: Oryan Date: Mon, 12 Oct 2026 15:29:10 -0400 Subject: [PATCH 2/4] Tighten the shared dictionary class tests The test for an input type and an object type sharing a class bound the input type through a resolver parameter, so its dictionary entry was never read. It now only uses the input type in a directive, which needs the entry. The test for object types sharing a class also maps an input type to that class, to check it is left out of the error. The scanner comment no longer says every other kind of type needs a class of its own. Scalars don't, but they keep the old rule. Co-Authored-By: Claude Opus 5.5 --- .../kickstart/tools/SchemaClassScanner.kt | 5 ++- .../tools/SchemaParserDictionaryTest.kt | 37 ++++++++----------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index 91cd8ed3..5b081e2d 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -59,8 +59,9 @@ internal class SchemaClassScanner( } } - // Input types never go into the type => class bimap, so several of them can share a class, e.g. ObjectNode. - // Other types need one each. + // Input types never go into the type => class bimap, so several of them can share a class, e.g. ObjectNode. Every + // other kind still gets one class per type, as before. Most of them end up in the bimap, which interface and union + // type resolution use to find the type of an object. initialDictionary .filterKeys { definitionsByName[it] !is InputObjectTypeDefinition } .entries.groupBy({ it.value }, { it.key }) diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt index 9629e9c3..625decea 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt @@ -164,15 +164,14 @@ class SchemaParserDictionaryTest { @Test fun `input type and object type can share a dictionary class`() { + // ItemInput is only used by a directive, so its class can only come from the dictionary val schema = SchemaParser.newParser() .schemaString( """ - type Query { - node: Node - } + directive @d(arg: ItemInput) on FIELD_DEFINITION - type Mutation { - save(item: ItemInput!): Node + type Query { + node: Node @d(arg: { name: "z" }) } interface Node { @@ -189,16 +188,17 @@ class SchemaParserDictionaryTest { """) .resolvers(object : GraphQLQueryResolver { fun node(): Node = Item().apply { name = "a" } - }, object : GraphQLMutationResolver { - fun save(item: Item): Node = item }) .dictionary("Item", Item::class.java) .dictionary("ItemInput", Item::class.java) .build() .makeExecutableSchema() - val gql = GraphQL.newGraphQL(schema).build() - val queryData = assertNoGraphQlErrors(gql) { + val field = schema.queryType.getFieldDefinition("node") + assertEquals(field.getAppliedDirective("d").getArgument("arg")!!.getValue>(), mapOf("name" to "z")) + + val gql = GraphQL.newGraphQL(schema).build() + val data = assertNoGraphQlErrors(gql) { """ query { node { @@ -208,19 +208,8 @@ class SchemaParserDictionaryTest { } """ } - val mutationData = assertNoGraphQlErrors(gql) { - """ - mutation { - save(item: { name: "b" }) { - __typename - name - } - } - """ - } - assertEquals(queryData["node"], mapOf("__typename" to "Item", "name" to "a")) - assertEquals(mutationData["save"], mapOf("__typename" to "Item", "name" to "b")) + assertEquals(data["node"], mapOf("__typename" to "Item", "name" to "a")) } @Test @@ -241,16 +230,22 @@ class SchemaParserDictionaryTest { type B { name: String } + + input AInput { + name: String + } """) .resolvers(object : GraphQLQueryResolver { fun a(): Item? = null fun b(): Item? = null }) .dictionary("A", Item::class.java) + .dictionary("AInput", Item::class.java) .dictionary("B", Item::class.java) .build() } + // the input type sharing the class isn't part of the conflict assertEquals(error.message, "Class in supplied dictionary '${Item::class.java.name}' specified type names 'A', 'B', " + "but only input object types can share a class!") } From ca2f1606e2b143ff363cd4c6e7fe5b9c66e34240 Mon Sep 17 00:00:00 2001 From: Oryan Date: Sat, 17 Oct 2026 11:55:58 -0400 Subject: [PATCH 3/4] Simplify the shared dictionary class check and fold its tests Co-Authored-By: Claude Opus 5.5 --- .../kickstart/tools/SchemaClassScanner.kt | 13 +- .../tools/SchemaParserDictionaryTest.kt | 203 ++++++------------ 2 files changed, 66 insertions(+), 150 deletions(-) diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index 3816e1f0..ad13a6d1 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -59,16 +59,15 @@ internal class SchemaClassScanner( } } - // Input types never go into the type => class bimap, so several of them can share a class, e.g. ObjectNode. Every - // other kind still gets one class per type, as before. Most of them end up in the bimap, which interface and union - // type resolution use to find the type of an object. + // Input object types never go into the type => class bimap that interface and union type resolution use, so several of + // them can share a class, e.g. ObjectNode initialDictionary .filterKeys { definitionsByName[it] !is InputObjectTypeDefinition } .entries.groupBy({ it.value }, { it.key }) - .filterValues { it.size > 1 } - .entries.firstOrNull() - ?.let { (clazz, names) -> - throw SchemaClassScannerError("Class in supplied dictionary '${clazz.name}' specified type names ${names.joinToString { "'$it'" }}, but only input object types can share a class!") + .forEach { (clazz, names) -> + if (names.size > 1) { + throw SchemaClassScannerError("Class in supplied dictionary '${clazz.name}' specified type names ${names.joinToString { "'$it'" }}, but only input object types can share a class!") + } } if (options.allowUnimplementedResolvers) { diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt index 625decea..01d18e9b 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt @@ -7,84 +7,23 @@ import org.junit.Test class SchemaParserDictionaryTest { - private val entitySchema = """ - type Query { - test: String - } - - type Mutation { - create(input: EntityCreateInput!): String - update(input: EntityUpdateInput!): String - } - - input EntityCreateInput { - name: String - nested: Nested - } - - input EntityUpdateInput { - id: ID! - nested: Nested - } - - input Nested { - x: Int - } - """ - - private val directiveSchema = """ - directive @a(arg: AInput) on FIELD_DEFINITION - directive @b(arg: BInput) on FIELD_DEFINITION - - type Query { - test: String @a(arg: { x: 1 }) @b(arg: { y: "b" }) - } - - input AInput { - x: Int - } - - input BInput { - y: String - } - """ - @Test fun `input types can share a dictionary class`() { - val schema = SchemaParser.newParser() - .schemaString(entitySchema) - .resolvers(EntityQuery(), EntityMutation()) - .dictionary("EntityCreateInput", ObjectNode::class.java) - .dictionary("EntityUpdateInput", ObjectNode::class.java) - .build() - .makeExecutableSchema() - val gql = GraphQL.newGraphQL(schema).build() - - val data = assertNoGraphQlErrors(gql) { - """ - mutation { - create(input: { name: "n", nested: { x: 1 } }) - update(input: { id: "2", nested: { x: 3 } }) - } - """ - } - - assertEquals(data["create"], """{"name":"n","nested":{"x":1}}""") - assertEquals(data["update"], """{"id":"2","nested":{"x":3}}""") - } - - @Test - fun `nested input types can share a dictionary class`() { val schema = SchemaParser.newParser() .schemaString( """ type Query { - create(input: EntityInput!): String + create(input: EntityCreateInput!): String + update(input: EntityUpdateInput!): String } - input EntityInput { + input EntityCreateInput { name: String a: NestedA + } + + input EntityUpdateInput { + id: ID! b: [NestedB!] } @@ -98,7 +37,11 @@ class SchemaParserDictionaryTest { """) .resolvers(object : GraphQLQueryResolver { fun create(input: ObjectNode): String = input.toString() + fun update(input: ObjectNode): String = input.toString() }) + .dictionary("EntityCreateInput", ObjectNode::class.java) + .dictionary("EntityUpdateInput", ObjectNode::class.java) + // ObjectNode has no properties, so the classes of the nested inputs come from the dictionary .dictionary("NestedA", ObjectNode::class.java) .dictionary("NestedB", ObjectNode::class.java) .build() @@ -108,42 +51,18 @@ class SchemaParserDictionaryTest { val data = assertNoGraphQlErrors(gql) { """ query { - create(input: { name: "n", a: { x: 1 }, b: [{ y: 2 }] }) + create(input: { name: "n", a: { x: 1 } }) + update(input: { id: "2", b: [{ y: 3 }] }) } """ } - assertEquals(data["create"], """{"name":"n","a":{"x":1},"b":[{"y":2}]}""") + assertEquals(data["create"], """{"name":"n","a":{"x":1}}""") + assertEquals(data["update"], """{"id":"2","b":[{"y":3}]}""") } @Test - fun `directive input types can share a dictionary class`() { - val schema = SchemaParser.newParser() - .schemaString(directiveSchema) - .resolvers(DirectiveQuery()) - .dictionary("AInput", Map::class.java) - .dictionary("BInput", Map::class.java) - .build() - .makeExecutableSchema() - - val field = schema.queryType.getFieldDefinition("test") - assertEquals(field.getAppliedDirective("a").getArgument("arg")!!.getValue>(), mapOf("x" to 1)) - assertEquals(field.getAppliedDirective("b").getArgument("arg")!!.getValue>(), mapOf("y" to "b")) - - val gql = GraphQL.newGraphQL(schema).build() - val data = assertNoGraphQlErrors(gql) { - """ - query { - test - } - """ - } - - assertEquals(data["test"], "test") - } - - @Test - fun `input types sharing a class can be passed to the builder from a SchemaParserDictionary`() { + fun `directive input types sharing a class can be passed to the builder from a SchemaParserDictionary`() { val dictionary = SchemaParserDictionary() .add("AInput", Map::class) .add("BInput", Map::class) @@ -152,56 +71,75 @@ class SchemaParserDictionaryTest { // graphql-spring-boot reads a SchemaParserDictionary bean this way val schema = SchemaParser.newParser() - .schemaString(directiveSchema) - .resolvers(DirectiveQuery()) + .schemaString( + """ + directive @a(arg: AInput) on FIELD_DEFINITION + directive @b(arg: BInput) on FIELD_DEFINITION + + type Query { + test: String @a(arg: { x: 1 }) @b(arg: { y: "b" }) + } + + input AInput { + x: Int + } + + input BInput { + y: String + } + """) + .resolvers(object : GraphQLQueryResolver { + fun test(): String? = null + }) .dictionary(dictionary.getDictionary()) .build() .makeExecutableSchema() val field = schema.queryType.getFieldDefinition("test") - assertEquals(field.getAppliedDirective("b").getArgument("arg")!!.getValue>(), mapOf("y" to "b")) + assertEquals(field.getAppliedDirective("a").getArgument("arg")?.getValue>(), mapOf("x" to 1)) + assertEquals(field.getAppliedDirective("b").getArgument("arg")?.getValue>(), mapOf("y" to "b")) } @Test fun `input type and object type can share a dictionary class`() { - // ItemInput is only used by a directive, so its class can only come from the dictionary + // DogInput is only used by a directive, so its class can only come from the dictionary val schema = SchemaParser.newParser() .schemaString( """ - directive @d(arg: ItemInput) on FIELD_DEFINITION + directive @d(arg: DogInput) on FIELD_DEFINITION type Query { - node: Node @d(arg: { name: "z" }) + dog: Dog @d(arg: { name: "z" }) } - interface Node { + interface Dog { name: String } - type Item implements Node { + type LabradorRetriever implements Dog { name: String } - input ItemInput { + input DogInput { name: String } """) .resolvers(object : GraphQLQueryResolver { - fun node(): Node = Item().apply { name = "a" } + fun dog(): Dog = LabradorRetriever("a", "black", 1, 1f) }) - .dictionary("Item", Item::class.java) - .dictionary("ItemInput", Item::class.java) + .dictionary(LabradorRetriever::class) + .dictionary("DogInput", LabradorRetriever::class) .build() .makeExecutableSchema() - val field = schema.queryType.getFieldDefinition("node") - assertEquals(field.getAppliedDirective("d").getArgument("arg")!!.getValue>(), mapOf("name" to "z")) + val field = schema.queryType.getFieldDefinition("dog") + assertEquals(field.getAppliedDirective("d").getArgument("arg")?.getValue>(), mapOf("name" to "z")) val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { """ query { - node { + dog { __typename name } @@ -209,7 +147,7 @@ class SchemaParserDictionaryTest { """ } - assertEquals(data["node"], mapOf("__typename" to "Item", "name" to "a")) + assertEquals(data["dog"], mapOf("__typename" to "LabradorRetriever", "name" to "a")) } @Test @@ -224,50 +162,29 @@ class SchemaParserDictionaryTest { } type A { - name: String + id: Int } type B { - name: String + id: Int } input AInput { - name: String + id: Int } """) .resolvers(object : GraphQLQueryResolver { - fun a(): Item? = null - fun b(): Item? = null + fun a(): ThirdItem? = null + fun b(): ThirdItem? = null }) - .dictionary("A", Item::class.java) - .dictionary("AInput", Item::class.java) - .dictionary("B", Item::class.java) + .dictionary("A", ThirdItem::class) + .dictionary("AInput", ThirdItem::class) + .dictionary("B", ThirdItem::class) .build() } // the input type sharing the class isn't part of the conflict - assertEquals(error.message, "Class in supplied dictionary '${Item::class.java.name}' specified type names 'A', 'B', " + + assertEquals(error.message, "Class in supplied dictionary '${ThirdItem::class.java.name}' specified type names 'A', 'B', " + "but only input object types can share a class!") } - - class EntityQuery : GraphQLQueryResolver { - fun test(): String? = null - } - - class EntityMutation : GraphQLMutationResolver { - fun create(input: ObjectNode): String = input.toString() - fun update(input: ObjectNode): String = input.toString() - } - - class DirectiveQuery : GraphQLQueryResolver { - fun test(): String = "test" - } - - interface Node { - val name: String? - } - - class Item : Node { - override var name: String? = null - } } From 446416b663b5c70236b9a953d35267ecacae0df3 Mon Sep 17 00:00:00 2001 From: Oryan Date: Sat, 17 Oct 2026 12:08:10 -0400 Subject: [PATCH 4/4] Name the shared dictionary class in its test Co-Authored-By: Claude Opus 5.5 --- .../graphql/kickstart/tools/SchemaParserDictionaryTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt index 01d18e9b..fffc1fbd 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt @@ -125,9 +125,9 @@ class SchemaParserDictionaryTest { } """) .resolvers(object : GraphQLQueryResolver { - fun dog(): Dog = LabradorRetriever("a", "black", 1, 1f) + fun dog(): Dog = LabradorRetriever(name = "a", color = "black", fluffiness = 1, speed = 1f) }) - .dictionary(LabradorRetriever::class) + .dictionary("LabradorRetriever", LabradorRetriever::class) .dictionary("DogInput", LabradorRetriever::class) .build() .makeExecutableSchema()