Skip to content

Variables

Purpose: Enable dynamic field selection and conditional GraphQL queries through runtime variable computation.

Usage: Variables can be bound to resolver arguments or computed dynamically using VariableProvider classes to control which fields are selected at the GraphQL execution level.

Viaduct supports three approaches for dynamic field resolution:

1. Variables with @Variable and fromArgument

Java uses viaduct.java.api.annotations.Variable inside @Resolver.variables. In the example below, fromArgument = "includeDetails" supplies the Boolean used by @include; only selected detail fields may be read when it is true.

Variables can be bound directly to resolver arguments to control GraphQL directive evaluation:

@Resolver(
    """
    fragment _ on Character {
        name
        birthYear @include(if: ${'$'}includeDetails)
        height @include(if: ${'$'}includeDetails)
        mass @include(if: ${'$'}includeDetails)
    }
    """,
    variables = [Variable("includeDetails", fromArgument = "includeDetails")]
)
@Prototype
class ProfileFieldResolver : CharacterResolvers.CharacterProfile() {
    override suspend fun resolve(ctx: Context): String? {
        val character = ctx.getObjectValue()
        val name = character.getNameOrThrow() ?: "Unknown"

        return try {
            // If includeDetails is true, these fields will be available
            val birthYear = character.getBirthYearOrThrow()
            val height = character.getHeightOrThrow()
            val mass = character.getMassOrThrow()

            buildString {
                append("Character Profile: $name")
                birthYear?.let { append(", Born: $it") }
                height?.let { append(", Height: ${it}cm") }
                mass?.let { append(", Mass: ${it}kg") }
            }
        } catch (e: Exception) {
            // If includeDetails is false, detailed fields won't be available
            "Character Profile: $name (basic info only)"
        }
    }
}

View full file on GitHub

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

/** Uses an argument-backed variable to conditionally select profile details. */
@Resolver(
    objectValueFragment =
        """
        fragment _ on Character {
          name
          birthYear @include(if: $includeDetails)
          height @include(if: $includeDetails)
          mass @include(if: $includeDetails)
        }
        """,
    variables = @Variable(name = "includeDetails", fromArgument = "includeDetails"))
@Prototype
public final class ProfileFieldResolver extends CharacterResolvers.CharacterProfile {
  @Override
  public CompletableFuture<String> resolve(Context context) {
    viaduct.java.grts.Character character = context.getObjectValue();
    String name = character.getName() == null ? "Unknown" : character.getName();
    if (!context.getArguments().getIncludeDetails()) {
      return CompletableFuture.completedFuture("Character Profile: " + name + " (basic info only)");
    }
    StringBuilder result = new StringBuilder("Character Profile: ").append(name);
    if (character.getBirthYear() != null) {
      result.append(", Born: ").append(character.getBirthYear());
    }
    if (character.getHeight() != null) {
      result.append(", Height: ").append(character.getHeight()).append("cm");
    }
    if (character.getMass() != null) {
      result.append(", Mass: ").append(character.getMass()).append("kg");
    }
    return CompletableFuture.completedFuture(result.toString());
  }
}

View full file on GitHub

Benefits: GraphQL-level optimization, declarative field selection, efficient data fetching.

2. Argument-Based Statistics Logic

For practical demo purposes, the character stats use argument-based conditional logic:

@Resolver(
    """
    fragment _ on Character {
        name
        birthYear
        height
        species {
            name
        }
    }
    """
)
@Prototype
class CharacterStatsResolver : CharacterResolvers.CharacterStats() {
    override suspend fun resolve(ctx: Context): String? {
        val character = ctx.getObjectValue()
        val name = character.getNameOrThrow() ?: "Unknown"
        val args = ctx.arguments

        return try {
            buildString {
                append("Stats for $name")
                append(" (Age range: ${args.minAge}-${args.maxAge})")

                // Try to access conditional fields
                try {
                    val birthYear = character.getBirthYearOrThrow()
                    val height = character.getHeightOrThrow()
                    birthYear?.let { append(", Born: $it") }
                    height?.let { append(", Height: ${it}cm") }
                } catch (e: Exception) {
                    append(" - age details not available for this range")
                }

                try {
                    val species = character.getSpeciesOrThrow()
                    species?.getNameOrThrow()?.let { append(", Species: $it") }
                } catch (e: Exception) {
                    // Species not included for this age range
                }
            }
        } catch (e: Exception) {
            "Stats unavailable for $name"
        }
    }
}

View full file on GitHub

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

/** Builds a character statistics summary from selected fields and arguments. */
@Resolver(
    objectValueFragment = "fragment _ on Character { name birthYear height species { name } }")
@Prototype
public final class CharacterStatsResolver extends CharacterResolvers.CharacterStats {
  @Override
  public CompletableFuture<String> resolve(Context context) {
    viaduct.java.grts.Character character = context.getObjectValue();
    String name = character.getName() == null ? "Unknown" : character.getName();
    StringBuilder result =
        new StringBuilder("Stats for ")
            .append(name)
            .append(" (Age range: ")
            .append(context.getArguments().getMinAge())
            .append('-')
            .append(context.getArguments().getMaxAge())
            .append(')');
    if (character.getBirthYear() != null) {
      result.append(", Born: ").append(character.getBirthYear());
    }
    if (character.getHeight() != null) {
      result.append(", Height: ").append(character.getHeight()).append("cm");
    }
    if (character.getSpecies() != null && character.getSpecies().getName() != null) {
      result.append(", Species: ").append(character.getSpecies().getName());
    }
    return CompletableFuture.completedFuture(result.toString());
  }
}

View full file on GitHub

Benefits: Simple implementation, full access to all fields, easy to debug and maintain.

Note: The full VariableProvider API with dynamic computation is available in the complete Viaduct runtime but simplified here for demo clarity.

3. Argument-Based Conditional Logic

For simpler cases, traditional argument processing within resolvers:

@Resolver(
    """
    fragment _ on Character {
        ...CharacterIdentityFields
        ...CharacterAppearanceFields
    }
    """
)
@Prototype
class CharacterFormattedDescriptionResolver : CharacterResolvers.FormattedDescription() {
    override suspend fun resolve(ctx: Context): String? {
        val character = ctx.getObjectValue()
        val name = character.getNameOrThrow() ?: "Unknown"
        val format = ctx.arguments.format

        return when (format) {
            "detailed" -> {
                val birthYear = character.getBirthYearOrThrow()
                val eyeColor = character.getEyeColorOrThrow()
                val hairColor = character.getHairColorOrThrow()

                buildString {
                    append(name)
                    birthYear?.let { append(" (born $it)") }
                    if (eyeColor != null || hairColor != null) {
                        append(" - ")
                        eyeColor?.let { append("$it eyes") }
                        if (eyeColor != null && hairColor != null) append(", ")
                        hairColor?.let { append("$it hair") }
                    }
                }
            }

            "year-only" -> {
                val birthYear = character.getBirthYearOrThrow()
                birthYear?.let { "$name (born $it)" } ?: "$name (birth year unknown)"
            }

            "appearance-only" -> {
                val eyeColor = character.getEyeColorOrThrow()
                val hairColor = character.getHairColorOrThrow()

                buildString {
                    append(name)
                    if (eyeColor != null || hairColor != null) {
                        append(" - ")
                        eyeColor?.let { append("$it eyes") }
                        if (eyeColor != null && hairColor != null) append(", ")
                        hairColor?.let { append("$it hair") }
                    }
                }
            }

            else -> name // default format - just name
        }
    }
}

View full file on GitHub

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

Benefits: Simplicity, ordinary Kotlin or Java control flow, easy debugging.

Example Schema:

type Character {
  # Variables with fromArgument - demonstrates GraphQL-level field selection
  characterProfile(includeDetails: Boolean = false): String @resolver

  # Argument-based statistics - practical implementation for demos
  characterStats(minAge: Int, maxAge: Int): String @resolver

  # Argument-based conditional logic - flexible formatting
  formattedDescription(format: String = "default"): String @resolver
}

Query Examples

@Variable fromArgument

query BasicProfile {
  node(id: "Q2hhcmFjdGVyOjE=") {  # Luke Skywalker
    ... on Character {
      name
      characterProfile(includeDetails: false)
      # Result: "Character Profile: Luke Skywalker (basic info only)"
    }
  }
}

Include details example

query DetailedProfile {
  node(id: "Q2hhcmFjdGVyOjE=") {
    ... on Character {
      name
      characterProfile(includeDetails: true)
      # Result: "Character Profile: Luke Skywalker, Born: 19BBY, Height: 172cm, Mass: 77.0kg"
    }
  }
}

VariableProvider with dynamic computation

query CharacterStats {
  node(id: "Q2hhcmFjdGVyOjU=") {  # Obi-Wan Kenobi
    ... on Character {
      name
      characterStats(minAge: 25, maxAge: 100)
      # Result: "Stats for Obi-Wan Kenobi (Age range: 25-100), Born: 57BBY, Height: 182cm, Species: Human"
    }
  }
}

Argument-based conditional logic

query FormattedDescriptions {
  node(id: "Q2hhcmFjdGVyOjI=") {  # Princess Leia
    ... on Character {
      name
      detailed: formattedDescription(format: "detailed")
      # Result: "Princess Leia (born 19BBY) - brown eyes, brown hair"

      yearOnly: formattedDescription(format: "year-only")
      # Result: "Princess Leia (born 19BBY)"

      default: formattedDescription(format: "default")
      # Result: "Princess Leia"
    }
  }
}

Combined usage of all three approaches

query CombinedVariablesDemo {
  node(id: "Q2hhcmFjdGVyOjE=") {  # Luke Skywalker
    ... on Character {
      name

      # @Variable with fromArgument examples
      basicProfile: characterProfile(includeDetails: false)
      detailedProfile: characterProfile(includeDetails: true)

      # VariableProvider with dynamic computation
      youngStats: characterStats(minAge: 0, maxAge: 30)
      oldStats: characterStats(minAge: 30, maxAge: 100)

      # Argument-based conditional logic
      nameOnly: formattedDescription(format: "default")
      yearOnly: formattedDescription(format: "year-only")
      detailed: formattedDescription(format: "detailed")
    }
  }
}

For the full @Variable, VariableProvider, and built-in directive reference, see the Schema Reference developer docs.

Film Fragment Examples

query {
  allFilms(limit: 2) {
    # Standard fields
    title
    director

    # Shorthand fragment - delegates to title
    displayTitle

    # Full fragment - combines episode, title, director
    summary

    # Full fragment - production details
    productionDetails

    # Full fragment with character data
    characterCountSummary
  }
}