Skip to content

Named Fragments

Two resolvers on the Character type in the filmography tenant need the same fields: CharacterDisplaySummaryResolver builds a "name (birthYear)" string, and CharacterRichSummaryResolver combines name and birthYear with batch-loaded relationship data. Rather than repeat name birthYear in both resolvers' fragments, the demo declares a named fragment once and spreads it in both.

Experimental API

Named fragments are an experimental Viaduct API and may change in a future release.

Declaring the fragment

A named fragment is annotated with @GraphQLFragment and extends FragmentFromAnnotation<T> for the type it selects on — here, Character. Kotlin uses a singleton object; Java uses a class with the Java annotation and document base class:

@GraphQLFragment("fragment CharacterIdentityFields on Character { name birthYear }")
object CharacterIdentityFieldsFragment : FragmentFromAnnotation<Character>()

View full file on GitHub

package com.example.starwars.modules.filmography.characters.fragments;

/** Reusable identity fields for Java resolvers and operations. */
@GraphQLFragment("fragment CharacterIdentityFields on Character { name birthYear }")
public final class CharacterIdentityFieldsFragment extends FragmentFromAnnotation<Character> {}

View full file on GitHub

Viaduct discovers @GraphQLFragment declarations at build time and validates them against the schema. The fragment name (CharacterIdentityFields) must be unique within the tenant module.

Spreading it in a resolver

Spread the fragment with standard ...CharacterIdentityFields syntax inside a resolver's objectValueFragment. CharacterDisplaySummaryResolver reads exactly the two fields the fragment provides:

@Resolver("fragment _ on Character { ...CharacterIdentityFields }")
@Prototype
class CharacterDisplaySummaryResolver : CharacterResolvers.DisplaySummary() {
    override suspend fun resolve(ctx: Context): String? {
        val character = ctx.getObjectValue()

View full file on GitHub

package com.example.starwars.modules.filmography.characters.resolvers;

/** Demonstrates a named fragment in a Java resolver selection. */
@Resolver(objectValueFragment = "fragment _ on Character { ...CharacterIdentityFields }")
@Prototype
public final class CharacterDisplaySummaryResolver extends CharacterResolvers.DisplaySummary {
  @Override
  public CompletableFuture<String> resolve(Context context) {
    viaduct.java.grts.Character character = context.getObjectValue();
    String name = character.getName() == null ? "Unknown" : character.getName();
    String birthYear =
        character.getBirthYear() == null ? "Unknown birth year" : character.getBirthYear();
    return CompletableFuture.completedFuture(name + " (" + birthYear + ")");
  }
}

View full file on GitHub

Reusing it alongside inline selections

A spread can be combined with inline fields. CharacterRichSummaryResolver needs the same identity fields plus the character's id for its batch lookups, so it spreads the shared fragment and adds id inline:

@Resolver(objectValueFragment = "fragment _ on Character { id ...CharacterIdentityFields }")
class CharacterRichSummaryResolver(/* ... */) : CharacterResolvers.RichSummary() {
    override suspend fun batchResolve(contexts: List<Context>): List<FieldValue<String>> {
        // ctx.getObjectValue().getNameOrThrow() / .getBirthYearOrThrow() come from the shared fragment,
        // ctx.getObjectValue().getIdOrThrow() comes from the inline selection.
    }
}
package com.example.starwars.modules.filmography.characters.resolvers;

/** Batches a summary that combines selected fields with repository data. */
@Resolver(
    objectValueFragment =
        "fragment _ on Character { id homeworld { name } ...CharacterIdentityFields }")
@Prototype
public final class CharacterRichSummaryResolver extends CharacterResolvers.RichSummary {
  private final CharacterFilmsRepository characterFilmsRepository;

  @Inject
  public CharacterRichSummaryResolver(CharacterFilmsRepository characterFilmsRepository) {
    this.characterFilmsRepository = characterFilmsRepository;
  }

  @Override
  public CompletableFuture<Map<Context, String>> batchResolve(List<Context> contexts) {
    Map<Context, String> results = new IdentityHashMap<>();
    for (Context context : contexts) {
      viaduct.java.grts.Character objectValue = context.getObjectValue();
      String characterId = objectValue.getId().getInternalID();
      String name = objectValue.getName() == null ? "Unknown" : objectValue.getName();
      String birthYear =
          objectValue.getBirthYear() == null ? "Unknown" : objectValue.getBirthYear();
      viaduct.java.grts.Planet homeworldPlanet = objectValue.getHomeworld();
      String homeworld =
          homeworldPlanet == null || homeworldPlanet.getName() == null
              ? "Unknown world"
              : homeworldPlanet.getName();
      int filmCount = characterFilmsRepository.findFilmsByCharacterId(characterId).size();
      results.put(
          context,
          name + " (" + birthYear + ") from " + homeworld + ", appears in " + filmCount + " films");
    }
    return CompletableFuture.completedFuture(results);
  }
}

View full file on GitHub

Because both resolvers now depend on the same fragment, adding a field to CharacterIdentityFields makes it available in both places at once — keeping the two resolvers in sync.

Spreading multiple fragments

A selection set can spread more than one named fragment — that's standard GraphQL, and Viaduct merges all the spreads into the resolver's required selection set. The demo declares a second fragment for appearance fields:

@GraphQLFragment("fragment CharacterAppearanceFields on Character { eyeColor hairColor }")
object CharacterAppearanceFieldsFragment : FragmentFromAnnotation<Character>()

View full file on GitHub

package com.example.starwars.modules.filmography.characters.fragments;

/** Reusable appearance fields for Java resolvers and operations. */
@GraphQLFragment("fragment CharacterAppearanceFields on Character { eyeColor hairColor }")
public final class CharacterAppearanceFieldsFragment extends FragmentFromAnnotation<Character> {}

View full file on GitHub

CharacterFormattedDescriptionResolver needs identity and appearance fields, so it spreads both fragments in one selection set:

@Resolver("fragment _ on Character { ...CharacterIdentityFields ...CharacterAppearanceFields }")
class CharacterFormattedDescriptionResolver : CharacterResolvers.FormattedDescription() {
    override suspend fun resolve(ctx: Context): String? {
        val character = ctx.getObjectValue()
        // getNameOrThrow()/getBirthYearOrThrow() come from CharacterIdentityFields,
        // getEyeColorOrThrow()/getHairColorOrThrow() come from CharacterAppearanceFields.
        ...
    }
}
package com.example.starwars.modules.filmography.characters.resolvers;

/** Formats character details using two reusable named fragments. */
@Resolver(
    objectValueFragment =
        """
        fragment _ on Character {
          ...CharacterIdentityFields
          ...CharacterAppearanceFields
        }
        """)
@Prototype
public final class CharacterFormattedDescriptionResolver
    extends CharacterResolvers.FormattedDescription {
  @Override
  public CompletableFuture<String> resolve(Context context) {
    viaduct.java.grts.Character character = context.getObjectValue();
    String name = character.getName() == null ? "Unknown" : character.getName();
    String format = context.getArguments().getFormat();
    return CompletableFuture.completedFuture(
        switch (format) {
          case "detailed" ->
              appendAppearance(appendBirthYear(name, character.getBirthYear()), character);
          case "year-only" ->
              character.getBirthYear() == null
                  ? name + " (birth year unknown)"
                  : appendBirthYear(name, character.getBirthYear());
          case "appearance-only" -> appendAppearance(name, character);
          default -> name;
        });
  }

  private String appendBirthYear(String value, String birthYear) {
    return birthYear == null ? value : value + " (born " + birthYear + ")";
  }

  private String appendAppearance(String value, viaduct.java.grts.Character character) {
    if (character.getEyeColor() == null && character.getHairColor() == null) {
      return value;
    }
    StringBuilder result = new StringBuilder(value).append(" - ");
    if (character.getEyeColor() != null) {
      result.append(character.getEyeColor()).append(" eyes");
    }
    if (character.getEyeColor() != null && character.getHairColor() != null) {
      result.append(", ");
    }
    if (character.getHairColor() != null) {
      result.append(character.getHairColor()).append(" hair");
    }
    return result.toString();
  }
}

View full file on GitHub

You can spread any number of fragments, mix them with inline fields (as the id ...CharacterIdentityFields example above does), and even spread fragments that themselves spread other fragments — Viaduct resolves the spreads transitively. The only constraints: each spread must resolve to a @GraphQLFragment declared in the same tenant module, and the fragment names must be distinct.

When to reach for a named fragment

Use a named fragment when more than one resolver in the same module needs the same selection set. For a one-off selection used by a single resolver, prefer an inline fragment — a named fragment only pays off when it's reused.

For the full API reference and scoping rules, see the Named Fragments developer reference. See Best Practices for the consolidated fragment guidance.