From f6e7ca707bc457593159ec49b9034dd8f50629b1 Mon Sep 17 00:00:00 2001 From: Tony Ghita Date: Fri, 28 Aug 2026 14:31:06 -0700 Subject: [PATCH 1/4] Fix non-null union field implements nullable union interface field --- .../validation/TypesImplementInterfaces.java | 2 +- .../TypesImplementInterfacesTest.groovy | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/schema/validation/TypesImplementInterfaces.java b/src/main/java/graphql/schema/validation/TypesImplementInterfaces.java index 2717ed8e46..a6e7be9537 100644 --- a/src/main/java/graphql/schema/validation/TypesImplementInterfaces.java +++ b/src/main/java/graphql/schema/validation/TypesImplementInterfaces.java @@ -189,7 +189,7 @@ private SchemaValidationError error(String msg) { boolean isCompatible(GraphQLOutputType constraintType, GraphQLOutputType objectType) { if (isSameType(constraintType, objectType)) { return true; - } else if (constraintType instanceof GraphQLUnionType) { + } else if (constraintType instanceof GraphQLUnionType && objectType instanceof GraphQLObjectType) { return objectIsMemberOfUnion((GraphQLUnionType) constraintType, objectType); } else if (constraintType instanceof GraphQLInterfaceType && objectType instanceof GraphQLObjectType) { return objectImplementsInterface((GraphQLInterfaceType) constraintType, (GraphQLObjectType) objectType); diff --git a/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy b/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy index c504393cf2..adf6018e24 100644 --- a/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy +++ b/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy @@ -1,5 +1,6 @@ package graphql.schema.validation +import graphql.TestUtil import graphql.schema.GraphQLInterfaceType import graphql.schema.GraphQLObjectType import spock.lang.Specification @@ -311,6 +312,29 @@ class TypesImplementInterfacesTest extends Specification { !badErrorCollector.getErrors().isEmpty() } + def "non-null union field implements nullable union interface field"() { + when: + TestUtil.schema(''' + schema { query: B } + + union U = A + + interface I { + i: U + } + + type A { + a: String + } + + type B implements I { + i: U! + } + ''') + + then: + noExceptionThrown() + } def "field is a non null object"() { given: From 2e21d2a86462cd9f505da3e123d204256207525f Mon Sep 17 00:00:00 2001 From: Tony Ghita Date: Mon, 31 Aug 2026 08:33:08 -0700 Subject: [PATCH 2/4] Add test coverage for all nullabilty strengthening cases --- .../TypesImplementInterfacesTest.groovy | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy b/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy index adf6018e24..36ee18a06f 100644 --- a/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy +++ b/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy @@ -4,6 +4,7 @@ import graphql.TestUtil import graphql.schema.GraphQLInterfaceType import graphql.schema.GraphQLObjectType import spock.lang.Specification +import spock.lang.Unroll import static SchemaValidationErrorType.ObjectDoesNotImplementItsInterfaces import static graphql.Scalars.GraphQLBoolean @@ -312,28 +313,51 @@ class TypesImplementInterfacesTest extends Specification { !badErrorCollector.getErrors().isEmpty() } - def "non-null union field implements nullable union interface field"() { + @Unroll + def "implementing field can strengthen #kind nullability from #interfaceFieldType to #implementingFieldType"() { when: - TestUtil.schema(''' + TestUtil.schema(""" schema { query: B } - union U = A + enum E { + VALUE + } - interface I { - i: U + interface J { + value: String } - type A { - a: String + type A implements J { + value: String + } + + union U = A + + interface I { + value: $interfaceFieldType } type B implements I { - i: U! + value: $implementingFieldType } - ''') + """) then: noExceptionThrown() + + where: + kind | interfaceFieldType | implementingFieldType + "scalar" | "String" | "String!" + "enum" | "E" | "E!" + "object" | "A" | "A!" + "interface" | "J" | "J!" + "union" | "U" | "U!" + "list" | "[String]" | "[String]!" + "list item" | "[String]" | "[String!]" + "list and item" | "[String]" | "[String!]!" + "nested list" | "[[String]]" | "[[String!]!]!" + "list of unions" | "[U]" | "[U!]!" + "list of interfaces" | "[J]" | "[J!]!" } def "field is a non null object"() { From ed849630e4c15614a1698964edb9d3b4fe76f214 Mon Sep 17 00:00:00 2001 From: Tony Ghita Date: Mon, 31 Aug 2026 08:33:48 -0700 Subject: [PATCH 3/4] Move unwrap non-null first so we're closer to spec --- .../validation/TypesImplementInterfaces.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/graphql/schema/validation/TypesImplementInterfaces.java b/src/main/java/graphql/schema/validation/TypesImplementInterfaces.java index a6e7be9537..e99faa80cd 100644 --- a/src/main/java/graphql/schema/validation/TypesImplementInterfaces.java +++ b/src/main/java/graphql/schema/validation/TypesImplementInterfaces.java @@ -189,16 +189,6 @@ private SchemaValidationError error(String msg) { boolean isCompatible(GraphQLOutputType constraintType, GraphQLOutputType objectType) { if (isSameType(constraintType, objectType)) { return true; - } else if (constraintType instanceof GraphQLUnionType && objectType instanceof GraphQLObjectType) { - return objectIsMemberOfUnion((GraphQLUnionType) constraintType, objectType); - } else if (constraintType instanceof GraphQLInterfaceType && objectType instanceof GraphQLObjectType) { - return objectImplementsInterface((GraphQLInterfaceType) constraintType, (GraphQLObjectType) objectType); - } else if (constraintType instanceof GraphQLInterfaceType && objectType instanceof GraphQLInterfaceType) { - return interfaceImplementsInterface((GraphQLInterfaceType) constraintType, (GraphQLInterfaceType) objectType); - } else if (isList(constraintType) && isList(objectType)) { - GraphQLOutputType wrappedConstraintType = (GraphQLOutputType) unwrapOne(constraintType); - GraphQLOutputType wrappedObjectType = (GraphQLOutputType) unwrapOne(objectType); - return isCompatible(wrappedConstraintType, wrappedObjectType); } else if (isNonNull(objectType)) { GraphQLOutputType nullableConstraint; if (isNonNull(constraintType)) { @@ -208,6 +198,16 @@ boolean isCompatible(GraphQLOutputType constraintType, GraphQLOutputType objectT } GraphQLOutputType nullableObjectType = (GraphQLOutputType) unwrapOne(objectType); return isCompatible(nullableConstraint, nullableObjectType); + } else if (constraintType instanceof GraphQLUnionType) { + return objectIsMemberOfUnion((GraphQLUnionType) constraintType, objectType); + } else if (constraintType instanceof GraphQLInterfaceType && objectType instanceof GraphQLObjectType) { + return objectImplementsInterface((GraphQLInterfaceType) constraintType, (GraphQLObjectType) objectType); + } else if (constraintType instanceof GraphQLInterfaceType && objectType instanceof GraphQLInterfaceType) { + return interfaceImplementsInterface((GraphQLInterfaceType) constraintType, (GraphQLInterfaceType) objectType); + } else if (isList(constraintType) && isList(objectType)) { + GraphQLOutputType wrappedConstraintType = (GraphQLOutputType) unwrapOne(constraintType); + GraphQLOutputType wrappedObjectType = (GraphQLOutputType) unwrapOne(objectType); + return isCompatible(wrappedConstraintType, wrappedObjectType); } else { return false; } From 38e843e069a432aa5737e8eda0482dda580d9df7 Mon Sep 17 00:00:00 2001 From: Tony Ghita Date: Tue, 8 Sep 2026 09:28:22 -0700 Subject: [PATCH 4/4] Add test coverage for TypesImplementInterfaces branches --- .../TypesImplementInterfacesTest.groovy | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy b/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy index 36ee18a06f..4a2808affe 100644 --- a/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy +++ b/src/test/groovy/graphql/schema/validation/TypesImplementInterfacesTest.groovy @@ -1,8 +1,11 @@ package graphql.schema.validation import graphql.TestUtil +import graphql.schema.GraphQLCodeRegistry import graphql.schema.GraphQLInterfaceType import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLSchema +import graphql.schema.GraphQLTypeReference import spock.lang.Specification import spock.lang.Unroll @@ -89,6 +92,77 @@ class TypesImplementInterfacesTest extends Specification { "object type 'obj' does not implement interface 'Interface' because field 'argField2' is missing argument(s): 'arg2, arg3'")) } + def "types must explicitly implement transitive interfaces"() { + given: + def baseInterface = newInterface() + .name("BaseInterface") + .field(newFieldDefinition().name("name").type(GraphQLString)) + .build() + def childInterface = newInterface() + .name("ChildInterface") + .field(newFieldDefinition().name("name").type(GraphQLString)) + .withInterface(baseInterface) + .build() + def incompleteImplementation = newObject() + .name("IncompleteImplementation") + .field(newFieldDefinition().name("name").type(GraphQLString)) + .withInterface(childInterface) + .build() + def completeImplementation = newObject() + .name("CompleteImplementation") + .field(newFieldDefinition().name("name").type(GraphQLString)) + .withInterface(baseInterface) + .withInterface(childInterface) + .build() + def missingErrorCollector = new SchemaValidationErrorCollector() + def completeErrorCollector = new SchemaValidationErrorCollector() + + when: + new TypesImplementInterfaces().check(incompleteImplementation, missingErrorCollector) + new TypesImplementInterfaces().check(completeImplementation, completeErrorCollector) + + then: + missingErrorCollector.getErrors()*.description == [ + "object type 'IncompleteImplementation' must implement 'BaseInterface' because it is implemented by 'ChildInterface'" + ] + completeErrorCollector.getErrors().isEmpty() + } + + def "interfaces cannot circularly implement each other"() { + given: + def firstInterface = newInterface() + .name("FirstInterface") + .field(newFieldDefinition().name("name").type(GraphQLString)) + .withInterface(GraphQLTypeReference.typeRef("SecondInterface")) + .build() + def secondInterface = newInterface() + .name("SecondInterface") + .field(newFieldDefinition().name("name").type(GraphQLString)) + .withInterface(firstInterface) + .build() + def query = newObject() + .name("Query") + .field(newFieldDefinition().name("find").type(secondInterface)) + .build() + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(firstInterface, { null }) + .typeResolver(secondInterface, { null }) + .build() + + when: + GraphQLSchema.newSchema() + .query(query) + .codeRegistry(codeRegistry) + .build() + + then: + def exception = thrown(InvalidSchemaException) + exception.getErrors()*.description as Set == [ + "interface type 'FirstInterface' cannot implement 'SecondInterface' because that would result on a circular reference", + "interface type 'SecondInterface' cannot implement 'FirstInterface' because that would result on a circular reference" + ] as Set + } + def "field is object implementing interface"() { given: def person = newInterface() @@ -230,6 +304,19 @@ class TypesImplementInterfacesTest extends Specification { !badErrorCollector.getErrors().isEmpty() } + def "interface and list fields are incompatible with unrelated field types"() { + given: + def interfaceType = newInterface() + .name("Interface") + .field(newFieldDefinition().name("field").type(GraphQLString)) + .build() + def validator = new TypesImplementInterfaces() + + expect: + !validator.isCompatible(interfaceType, GraphQLString) + !validator.isCompatible(list(GraphQLString), GraphQLString) + } + def "field is member of union"() { given: def actor = newObject() @@ -419,6 +506,33 @@ class TypesImplementInterfacesTest extends Specification { errors.isEmpty() } + def "field argument defaults must be set in both the interface and implementing type"() { + given: + GraphQLInterfaceType interfaceType = newInterface() + .name("Interface") + .field(newFieldDefinition().name("argField").type(GraphQLString) + .argument(newArgument().name("interfaceDefault").type(GraphQLString).defaultValueProgrammatic("default")) + .argument(newArgument().name("objectDefault").type(GraphQLString))) + .build() + GraphQLObjectType objectType = newObject() + .name("Object") + .withInterface(interfaceType) + .field(newFieldDefinition().name("argField").type(GraphQLString) + .argument(newArgument().name("interfaceDefault").type(GraphQLString)) + .argument(newArgument().name("objectDefault").type(GraphQLString).defaultValueProgrammatic("default"))) + .build() + SchemaValidationErrorCollector errorCollector = new SchemaValidationErrorCollector() + + when: + new TypesImplementInterfaces().check(objectType, errorCollector) + + then: + errorCollector.getErrors()*.description as Set == [ + "object type 'Object' does not implement interface 'Interface' because field 'argField' argument 'interfaceDefault' is defined differently", + "object type 'Object' does not implement interface 'Interface' because field 'argField' argument 'objectDefault' is defined differently" + ] as Set + } + def "type should declare all arguments present in implemented interface"() { given: