OneOf Directive

Ensure exactly one non-null field in input objects; works well with @idOf for typed Global IDs.

The @oneOf directive enforces that exactly one field of an input object is non-null. This is useful for union-like inputs where callers choose one of several ways to identify or filter an entity.

Basic usage

input CharacterSearchInput @oneOf {
  byName: String
  byBirthYear: String
  byId: ID
}

type Query {
  searchCharacter(search: CharacterSearchInput!): Character
}

If the client provides zero or more than one field, the request is rejected during validation.

Combining @oneOf with @idOf

When one of the alternatives is a Global ID, add @idOf to make the ID typed and to enable automatic decoding.

input CharacterSearchInput @oneOf @scope(to: ["default"]) {
 """
 Search by character name
 """
 byName: String

 # tag::id_example[4] ID search sample
 """
 Search by character ID
 """
 byId: ID @idOf(type: "Character")
 """
 Search by birth year
 """
 byBirthYear: String
}

Client query examples:

# Option A: by name
query {
  searchCharacter(search: { byName: "Luke Skywalker" }) {
    id
    name
  }
}

# Option B: by Global ID (typed via @idOf)
query {
  searchCharacter(search: { byId: "Q2hhcmFjdGVyOjE=" }) {
    id
    name
  }
}

With @idOf, Viaduct will validate that the ID is a Character Global ID and decode it before your resolver runs.

Resolver pattern

Inside the resolver, inspect which field was set and branch accordingly. When the ID path is used, the internal ID is already validated and decoded by Viaduct.

@Resolver
class SearchCharacterQueryResolver : QueryResolvers.SearchCharacter() {
override suspend fun resolve(ctx: Context): viaduct.api.grts.Character? {
    val search = ctx.arguments.search
    val byName = search.byName
    val byId = search.byId
    val byBirthYear = search.byBirthYear

    val character = when {
        byName != null -> CharacterRepository.findCharactersByName(byName).firstOrNull()
        byId != null -> CharacterRepository.findById(byId.internalID)
        byBirthYear != null -> CharacterRepository.findCharactersByYearOfBirth(byBirthYear).firstOrNull()
        else -> null
    }

    return character?.let {
        CharacterBuilder(ctx).build(character)
    }
}

Error behavior

  • Zero fields provided: validation error (input must include exactly one field).
  • Multiple fields provided: validation error.
  • Wrong ID type: validation error before the resolver runs (thanks to @idOf).

Design guidelines

  • Keep input alternatives orthogonal (avoid overlapping semantics).
  • Use @idOf on the ID branch so the resolver receives a typed and decoded internal ID.
  • Return null for “not found” results; keep exceptions for unexpected failures.