Skip to content

Fragment on an interface that spreads a fragment on a parent interface drops the inheritance edge and the parent's fields from the base class #415

Description

@behnamsattar

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:

  1. does not implements Fragment$BFragment, and
  2. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions