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:
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> {}
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:
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 + ")");
}
}
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);
}
}
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:
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> {}
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();
}
}
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.