Summary
When a fragment defined on interface A spreads a fragment defined on a super-interface B (schema: A implements B), the generated base class Fragment$AFragment:
- does not
implements Fragment$BFragment, and
- does not carry B's fields.
The interface relationship and B's fields appear only on the per-possible-type variant classes Fragment$AFragment$$ConcreteType. So a value statically typed as the base A fragment can be neither upcast to B nor read for B's fields. You're forced to switch on __typename/a type enum and downcast to a concrete leaf by hand.
Minimal reproduction (valid schema)
# schema.graphql
interface Node { id: ID! }
interface Animal implements Node { id: ID! name: String! }
type Dog implements Animal & Node { id: ID! name: String! goodBoy: Boolean! }
type Query { animal: Animal! }
# operations.graphql
fragment NodeFragment on Node { id }
fragment AnimalFragment on Animal { ...NodeFragment name } # interface-fragment spread inside an interface fragment
query GetAnimal { animal { ...AnimalFragment } }
Expected
class Fragment$AnimalFragment implements Fragment$NodeFragment
Fragment$AnimalFragment exposes both id (from NodeFragment) and name.
Actual
class Fragment$AnimalFragment { … } implements nothing.
- It exposes only
name + $__typename; id exists only on Fragment$AnimalFragment$$Dog.
- To read
id from an AnimalFragment, you must is/cast to the $$Dog variant.
Root cause (located)
In lib/src/visitor/context_visitor.dart, visitFragmentSpreadNode returns early for any spread whose type condition isn't an exact match once the current type is abstract:
if (context.currentType is! ObjectTypeDefinitionNode) {
return; // ← abstract current type + super-interface spread → dropped entirely
}
The exact-match branch above it (_visitInFragment(...), which records both the fields and the implements edge via addFragment(...)) is what makes inheritance work, but it's skipped here, so super-interface spreads into an abstract fragment contribute nothing to the base context. The relationship survives only on the concrete $$ConcreteType variants built by _buildConcreteTypeContexts.
Environment
graphql_codegen 3.0.1, graphql 5.1.3, Dart 3.x.
Note for maintainers
A naive fix (hoist the fields + add the implements edge for the subsumes case) collides with the per-interface maybeWhen API: Fragment$AnimalFragment.maybeWhen takes Animal$$Dog callbacks while Fragment$NodeFragment.maybeWhen takes Node$$Dog callbacks, and since Animal$$Dog <: Node$$Dog, the sub-interface's maybeWhen would be a non-contravariant (illegal) override. So a complete fix also has to change where maybeWhen is emitted (e.g. emit it only on the root abstract type, or move discrimination to native is/switch over the variant lattice).
Summary
When a fragment defined on interface A spreads a fragment defined on a super-interface B (schema:
A implements B), the generated base classFragment$AFragment:implements Fragment$BFragment, andThe interface relationship and B's fields appear only on the per-possible-type variant classes
Fragment$AFragment$$ConcreteType. So a value statically typed as the baseAfragment can be neither upcast toBnor read for B's fields. You're forced toswitchon__typename/atypeenum and downcast to a concrete leaf by hand.Minimal reproduction (valid schema)
Expected
class Fragment$AnimalFragment implements Fragment$NodeFragmentFragment$AnimalFragmentexposes bothid(fromNodeFragment) andname.Actual
class Fragment$AnimalFragment { … }implements nothing.name+$__typename;idexists only onFragment$AnimalFragment$$Dog.idfrom anAnimalFragment, you mustis/cast to the$$Dogvariant.Root cause (located)
In
lib/src/visitor/context_visitor.dart,visitFragmentSpreadNodereturns early for any spread whose type condition isn't an exact match once the current type is abstract:The exact-match branch above it (
_visitInFragment(...), which records both the fields and theimplementsedge viaaddFragment(...)) is what makes inheritance work, but it's skipped here, so super-interface spreads into an abstract fragment contribute nothing to the base context. The relationship survives only on the concrete$$ConcreteTypevariants built by_buildConcreteTypeContexts.Environment
graphql_codegen 3.0.1,graphql 5.1.3, Dart 3.x.Note for maintainers
A naive fix (hoist the fields + add the
implementsedge for the subsumes case) collides with the per-interfacemaybeWhenAPI:Fragment$AnimalFragment.maybeWhentakesAnimal$$Dogcallbacks whileFragment$NodeFragment.maybeWhentakesNode$$Dogcallbacks, and sinceAnimal$$Dog <: Node$$Dog, the sub-interface'smaybeWhenwould be a non-contravariant (illegal) override. So a complete fix also has to change wheremaybeWhenis emitted (e.g. emit it only on the root abstract type, or move discrimination to nativeis/switchover the variant lattice).