Skip to content

Backport to 26.1: Fix covariance checks for SDL type extensions - #4444

Open
andimarek wants to merge 1 commit into
26.xfrom
codex/backport-4420-to-26.x
Open

Backport to 26.1: Fix covariance checks for SDL type extensions#4444
andimarek wants to merge 1 commit into
26.xfrom
codex/backport-4420-to-26.x

Conversation

@andimarek

Copy link
Copy Markdown
Member

Backport of #4420 to the 26.x maintenance branch.

Problem

GraphQL Java incorrectly rejected valid covariant field return types when the subtype relationship was introduced by an SDL extension rather than declared on the base type.

Object extension example

In this schema, Dog implements Pet through an extension. Returning Dog for a field declared as Pet is valid covariance:

type Query {
  base: Base
}

interface Pet {
  id: ID
}

type Dog {
  id: ID
}

extend type Dog implements Pet

type Base {
  foo: String
}

interface PetContainer {
  pet: Pet
}

extend type Base implements PetContainer {
  pet: Dog
}

The schema was incorrectly rejected because the covariance check only inspected the base definition of Dog and did not see extend type Dog implements Pet.

Interface extension example

The same problem affected interfaces inheriting from other interfaces through extensions:

type Query {
  base: Base
}

interface Pet {
  id: ID
}

interface WorkingPet {
  id: ID
}

extend interface WorkingPet implements Pet

interface PetContainer {
  pet: Pet
}

type Base implements PetContainer {
  pet: WorkingPet
}

WorkingPet is a valid covariant return type for Pet, but the extension-based relationship was previously ignored.

Union extension example

Union members introduced through extensions were also missed:

type Query {
  base: Base
}

type Cat {
  id: ID
}

type Dog {
  id: ID
}

union Pets = Cat

extend union Pets = Dog

interface PetContainer {
  pet: Pets
}

type Base implements PetContainer {
  pet: Dog
}

Dog is a valid subtype of Pets, but the previous check only inspected members declared on the base union.

Summary

TypeDefinitionRegistry stores base SDL definitions and extension definitions separately. This change makes the registry use the logical base-plus-extension relationships when:

  • checking whether an object or interface is a possible type of an interface
  • checking whether an object is a possible type of a union
  • checking covariant field return types, including nested list and non-null wrappers
  • finding object and interface implementations of an interface

This matches graphql-js behavior. graphql-js materializes extensions into its schema types before subtype checks; GraphQL Java retains separate AST definitions and now combines them when answering the equivalent registry queries.

Wrapped covariance

The same relationship is preserved while recursively checking list and non-null wrappers:

type Query {
  base: Base
}

interface Pet {
  id: ID
}

type Dog {
  id: ID
}

extend type Dog implements Pet

interface PetContainer {
  pets: [Pet]!
}

type Base implements PetContainer {
  pets: [Dog!]!
}

[Dog!]! remains a valid subtype of [Pet]!.

Unrelated types remain invalid

Extension relationships are matched by their declared interface and do not make unrelated types compatible:

type Query {
  base: Base
}

interface Pet {
  id: ID
}

interface Vehicle {
  id: ID
}

type Car {
  id: ID
}

extend type Car implements Vehicle

interface PetContainer {
  pet: Pet
}

type Base implements PetContainer {
  pet: Car
}

This schema is still rejected because Car implements Vehicle, not Pet.

@andimarek andimarek changed the title Fix covariance checks for SDL type extensions Backport to 26: Fix covariance checks for SDL type extensions Aug 19, 2026
@andimarek
andimarek force-pushed the codex/backport-4420-to-26.x branch from bd2f444 to 8f74506 Compare August 19, 2026 21:04
@github-actions

Copy link
Copy Markdown
Contributor

Test Report

Test Results

Java Version Total Passed Failed Errors Skipped
Java 11 5746 (-181 🔴) 5690 (-181 🔴) 0 (±0) 0 (±0) 56 (±0)
Java 17 5746 (-181 🔴) 5689 (-181 🔴) 0 (±0) 0 (±0) 57 (±0)
Java 21 5746 (-181 🔴) 5689 (-181 🔴) 0 (±0) 0 (±0) 57 (±0)
Java 25 5746 (-181 🔴) 5689 (-181 🔴) 0 (±0) 0 (±0) 57 (±0)
jcstress 32 (±0) 32 (±0) 0 (±0) 0 (±0) 0 (±0)
Total 23016 (-724 🔴) 22789 (-724 🔴) 0 (±0) 0 (±0) 227 (±0)

Code Coverage (Java 25)

Metric Covered Missed Coverage vs Master
Lines 28893 3119 90.3% -0.3% 🔴
Branches 8398 1502 84.8% -0.4% 🔴
Methods 7735 1222 86.4% -0.6% 🔴

Changed Class Coverage (34 classes)

Class Line Branch Method
g.l.AstSignature
$3
+100.0% 🟢 +100.0% 🟢 +100.0% 🟢
g.l.AstSignatureInputReferences removed removed removed
g.l.AstSignatureReferenceCollector removed removed removed
g.l.AstSignatureWithInputResult removed removed removed
g.l.AstSignatureWithInputResult
$Builder
removed removed removed
g.l.Directive -3.2% 🔴 ±0.0% -5.9% 🔴
g.l.DirectiveDefinition -19.2% 🔴 ±0.0% ±0.0%
g.l.DirectiveExtensionDefinition removed removed removed
g.l.DirectiveExtensionDefinition
$Builder
removed removed removed
g.l.DirectiveLocation -4.3% 🔴 ±0.0% -8.3% 🔴
g.l.FragmentSpread -16.1% 🔴 ±0.0% -27.8% 🔴
g.l.FragmentSpread
$Builder
-6.5% 🔴 ±0.0% -9.1% 🔴
g.l.PrettyAstPrinter ±0.0% -2.0% 🔴 ±0.0%
g.l.VariableDefinition -14.5% 🔴 ±0.0% -26.1% 🔴
g.l.VariableDefinition
$Builder
-6.2% 🔴 ±0.0% -8.8% 🔴
g.s.d.a.EditOperationAnalyzer -0.3% 🔴 ±0.0% -0.7% 🔴
g.s.d.a.SchemaDifference
$AppliedDirectiveDirectiveLocation
removed removed removed
g.s.GraphQLDirective -3.1% 🔴 ±0.0% -6.7% 🔴
g.s.GraphqlDirectivesContainerTypeBuilder -14.7% 🔴 -50.0% 🔴 -18.2% 🔴
g.s.GraphQLInterfaceType
$Builder
-1.4% 🔴 ±0.0% -3.8% 🔴
g.s.GraphQLObjectType -1.7% 🔴 ±0.0% -3.6% 🔴
g.s.i.e.DirectiveExtensionDirectiveRedefinitionError removed removed removed
g.s.i.e.DirectiveExtensionMissingBaseError removed removed removed
g.s.i.e.SchemaProblem -16.7% 🔴 ±0.0% -25.0% 🔴
g.s.i.UnionTypesChecker ±0.0% -7.1% 🔴 ±0.0%
g.s.t.FieldVisibilitySchemaTransformation
$ElementRemovalVisitor
removed removed removed
g.s.t.FieldVisibilitySchemaTransformation
$FieldRemovalVisitor
+100.0% 🟢 +100.0% 🟢 +100.0% 🟢
g.s.t.VisibleInterfaceImplementationPredicateEnvironmentImpl removed removed removed
g.s.v.NoDefaultValueCircularRefs removed removed removed
g.s.v.NoUnbrokenInputCycles -2.6% 🔴 -4.4% 🔴 ±0.0%
g.v.OperationValidator ±0.0% -1.3% 🔴 ±0.0%
g.v.TraversalContext +99.4% 🟢 +94.1% 🟢 +100.0% 🟢
g.v.ValidationContext -2.8% 🔴 ±0.0% -5.0% 🔴
g.v.ValidationError
$Builder
±0.0% -50.0% 🔴 ±0.0%
Directive — method details
Method Line Branch
deepCopy 0.0% (-100.0% 🔴)
DirectiveDefinition — method details
Method Line Branch
<init> new 100.0%
withNewChildren 0.0% (-100.0% 🔴)
deepCopy 0.0% (-100.0% 🔴)
lambda$withNewChildren$0 0.0% (-100.0% 🔴)
<init> removed removed
getDirectives removed removed
getDirectivesByName removed removed
getDirectives removed removed
hasDirective removed removed
DirectiveLocation — method details
Method Line Branch
deepCopy 0.0% (-100.0% 🔴)
FragmentSpread — method details
Method Line Branch
getDirectivesByName 0.0% (-100.0% 🔴)
getDirectives 0.0% (-100.0% 🔴)
hasDirective 0.0% (-100.0% 🔴)
withNewChildren 0.0% (-100.0% 🔴)
lambda$withNewChildren$0 0.0% (-100.0% 🔴)
FragmentSpread.Builder — method details
Method Line Branch
directive 0.0% (-100.0% 🔴)
PrettyAstPrinter — method details
Method Line Branch
lambda$directiveDefinition$0 100.0% 50.0% (-50.0% 🔴)
directiveExtensionDefinition removed removed
lambda$directiveExtensionDefinition$0 removed removed
VariableDefinition — method details
Method Line Branch
<init> new 100.0%
getDirectivesByName 0.0% (-100.0% 🔴)
getDirectives 0.0% (-100.0% 🔴)
hasDirective 0.0% (-100.0% 🔴)
withNewChildren 0.0% (-100.0% 🔴)
newVariableDefinition 0.0% (-100.0% 🔴)
lambda$withNewChildren$0 0.0% (-100.0% 🔴)
<init> removed removed
VariableDefinition.Builder — method details
Method Line Branch
directive 0.0% (-100.0% 🔴)
description removed removed
EditOperationAnalyzer — method details
Method Line Branch
appliedDirectiveDeleted 85.5% 72.5% (-0.2% 🔴)
appliedDirectiveArgumentDeleted 82.6% (-0.3% 🔴) 73.5% (-0.6% 🔴)
appliedDirectiveArgumentAdded 79.6% (-0.4% 🔴) 69.4% (-0.8% 🔴)
appliedDirectiveArgumentChanged 99.1% 68.6% (-0.3% 🔴)
appliedDirectiveAdded 85.5% 72.5% (-0.2% 🔴)
isAppliedDirectiveAdded 80.4% (-8.9% 🔴) 78.6% (-4.8% 🔴)
lambda$isAppliedDirectiveAdded$6 0.0% (-100.0% 🔴)
GraphQLDirective — method details
Method Line Branch
<init> new 100.0%
copy 0.0% (-100.0% 🔴)
<init> removed removed
getExtensionDefinitions removed removed
isDeprecated removed removed
getDeprecationReason removed removed
getDirectives removed removed
getDirectivesByName removed removed
getAllDirectivesByName removed removed
getDirective removed removed
getAppliedDirectives removed removed
getAllAppliedDirectivesByName removed removed
getAppliedDirective removed removed
GraphqlDirectivesContainerTypeBuilder — method details
Method Line Branch
withDirectives 0.0% (-100.0% 🔴) 0.0% (-100.0% 🔴)
withDirective 0.0% (-100.0% 🔴)
GraphQLInterfaceType.Builder — method details
Method Line Branch
replaceInterfaces 0.0% (-100.0% 🔴)
GraphQLObjectType — method details
Method Line Branch
getDirective 0.0% (-100.0% 🔴)
SchemaProblem — method details
Method Line Branch
toString 0.0% (-100.0% 🔴)
UnionTypesChecker — method details
Method Line Branch
checkUnionType 100.0% 90.0% (-10.0% 🔴)
hasMemberTypes removed removed
lambda$hasMemberTypes$0 removed removed
NoUnbrokenInputCycles — method details
Method Line Branch
unwrapNonNull 83.3% (-16.7% 🔴) 75.0% (-25.0% 🔴)
OperationValidator — method details
Method Line Branch
checkVariable new 100.0% 100.0%
validateDeferDirectiveOnRootLevel 92.9% 68.8% (-12.5% 🔴)
validateDeferDirectiveOnValidOperation new 90.9% 80.0%
getOperationDefinition new 100.0%
lambda$getOperationDefinition$1 new 100.0%
lambda$getOperationDefinition$0 new 100.0%
enterTraversal removed removed
enterField removed removed
enterVariableDefinition removed removed
enterArgument removed removed
enterArrayValue removed removed
enterObjectField removed removed
leaveTraversal removed removed
resolveSelectionSetParent removed removed
getSelectionSetOwnerType removed removed
getOperationType removed removed
getFieldDefinition removed removed
getArgumentDefinition removed removed
getNullableType removed removed
getParentType removed removed
getFieldDefinition removed removed
getInputType removed removed
getDefaultValue removed removed
getDirectiveDefinition removed removed
enterName removed removed
leaveName removed removed
lastElement removed removed
pop removed removed
checkVariable removed removed
validateVariableNotAllowedInConstantDirective removed removed
getInlineFragmentType removed removed
validateDeferDirectiveOnValidOperation removed removed
ValidationContext — method details
Method Line Branch
getTraversalContext new 100.0%
getParentType new 100.0%
getInputType new 100.0%
getDefaultValue new 100.0%
getFieldDef new 100.0%
getDirective new 100.0%
getArgument new 100.0%
getOutputType new 100.0%
getQueryPath new 100.0%
toString new 0.0%
ValidationError.Builder — method details
Method Line Branch
sourceLocation 100.0% 50.0% (-50.0% 🔴)

Full HTML report: build artifact jacoco-html-report

Updated: 2026-08-19 21:13:43 UTC

@andimarek andimarek changed the title Backport to 26: Fix covariance checks for SDL type extensions Backport to 26.1: Fix covariance checks for SDL type extensions Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant