diff --git a/v2/pkg/astvalidation/operation_rule_field_selection_merging.go b/v2/pkg/astvalidation/operation_rule_field_selection_merging.go index fd35b08abd..c8b8a079cf 100644 --- a/v2/pkg/astvalidation/operation_rule_field_selection_merging.go +++ b/v2/pkg/astvalidation/operation_rule_field_selection_merging.go @@ -241,14 +241,20 @@ func (f *fieldSelectionMergingVisitor) EnterField(ref int) { // to the same runtime object. This determines whether field merging must enforce // strict type equality (including nullability) or may relax it. // -// - If either type is an interface, returns true (conservative: any concrete -// type might implement that interface). +// - If one type is an interface and the other is an object, returns true only +// when the object type implements the interface (otherwise they cannot overlap). +// - If both types are interfaces, returns true (conservative: some concrete +// type might implement both). // - Two object types overlap only when they share the same name. // - All other combinations return false. func (f *fieldSelectionMergingVisitor) potentiallySameObject(left, right ast.Node) bool { switch { - case left.Kind == ast.NodeKindInterfaceTypeDefinition || right.Kind == ast.NodeKindInterfaceTypeDefinition: + case left.Kind == ast.NodeKindInterfaceTypeDefinition && right.Kind == ast.NodeKindInterfaceTypeDefinition: return true + case left.Kind == ast.NodeKindInterfaceTypeDefinition && right.Kind == ast.NodeKindObjectTypeDefinition: + return f.definition.NodeImplementsInterfaceNode(right, left) + case left.Kind == ast.NodeKindObjectTypeDefinition && right.Kind == ast.NodeKindInterfaceTypeDefinition: + return f.definition.NodeImplementsInterfaceNode(left, right) case left.Kind == ast.NodeKindObjectTypeDefinition && right.Kind == ast.NodeKindObjectTypeDefinition: return bytes.Equal(f.definition.ObjectTypeDefinitionNameBytes(left.Ref), f.definition.ObjectTypeDefinitionNameBytes(right.Ref)) default: diff --git a/v2/pkg/astvalidation/operation_validation_test.go b/v2/pkg/astvalidation/operation_validation_test.go index 6691f12114..cd4fe494a9 100644 --- a/v2/pkg/astvalidation/operation_validation_test.go +++ b/v2/pkg/astvalidation/operation_validation_test.go @@ -1116,6 +1116,33 @@ func TestExecutionValidation(t *testing.T) { }`, FieldSelectionMerging(), Invalid, withValidationErrors(`fields 'scalar' conflict because they return conflicting types 'String!' and 'String'`)) }) + t.Run("allows differing return type nullability on interface vs non implementing type with relaxation", func(t *testing.T) { + runWithDefinition(t, boxDefinition, ` + { + someBox { + ... on NonNullStringBox1 { + scalar + } + ... on StringBox { + scalar + } + } + }`, FieldSelectionMerging(true), Valid) + }) + t.Run("rejects differing return type nullability on interface vs implementing type even with relaxation", func(t *testing.T) { + runWithDefinition(t, boxDefinition, ` + { + someBox { + ... on SomeBox { + scalar + } + ... on NonNullStringBox1Impl { + scalar + } + } + }`, FieldSelectionMerging(true), Invalid, + withValidationErrors(`fields 'scalar' conflict because they return conflicting types 'String' and 'String!'`)) + }) t.Run("same wrapped scalar return types", func(t *testing.T) { runWithDefinition(t, boxDefinition, ` {