idOf Directive
The @idOf directive binds an ID field or argument to a specific GraphQL type, allowing Viaduct to perform
automatic type validation and Global ID decoding. It ensures that the ID belongs to the expected type before invoking
your resolver, preventing mismatched or malformed identifiers at runtime.
Why it matters
In GraphQL, all ID values are strings. Without additional metadata, there’s no way to know which entity type an ID
represents. @idOf introduces type awareness by declaring that a given ID corresponds to a specific GraphQL type.
This allows Viaduct to:
- Validate incoming IDs before they reach resolver logic.
- Decode base64-encoded Global IDs automatically.
- Reject mismatched IDs (for example, passing a
PlanetID to aCharacterresolver). - Generate type-safe schemas that tools can reason about statically.
Basic usage
Apply @idOf to any ID argument or field that represents a Global ID.
"""
Search by character ID
"""
byId: ID @idOf(type: "Character")
type Character implements Node @scope(to: ["default"]) @resolver {
"""
The GlobalID of this character - uniquely identifies this Character in the graph (internal use only)
"""
id: ID!
How it works at runtime
When a client calls a query such as:
query {
character(id: "Q2hhcmFjdGVyOjE=") {
id
name
}
}
Viaduct will:
- Decode the base64 string
"Q2hhcmFjdGVyOjE="→"Character:1". - Validate that the declared type (
Character) matches the type in the encoded ID. - Populate
ctx.id.internalIDwith"1". - Pass control to
CharacterNodeResolver, where you can safely use the internal ID.
@Resolver
class CharacterNodeResolver : NodeResolvers.Character() {
// Node resolvers can also be batched to optimize multiple requests
// tag::node_batch_resolver_example[21] Example of a node resolver
override suspend fun batchResolve(contexts: List<Context>): List<FieldValue<Character>> {
// Extract all unique character IDs from the contexts
val characterIds = contexts.map { it.id.internalID }
// Perform a single batch query to get film counts for all characters
// We only compute one time for each character, despite multiple requests
val characters = characterIds.mapNotNull {
CharacterRepository.findById(it)
}
// For each context gets the character ID and map to the viaduct object
return contexts.map { ctx ->
val characterId = ctx.id.internalID
This pattern ensures that only valid, correctly-typed IDs reach your business logic.
Advantages
- Eliminates manual parsing of base64 Global IDs.
- Prevents runtime errors caused by type mismatches.
- Simplifies schema introspection and static analysis.
- Makes field-level validation explicit and discoverable in the schema.
Common mistakes
1. Using @idOf on non-ID fields
The directive should only decorate ID fields or arguments. Applying it to String or Int fields has no effect and
may produce schema validation warnings.
2. Forgetting @idOf on inputs that expect Global IDs
If an argument or input field represents a Global ID but lacks @idOf, Viaduct treats it as a plain string, skipping
type validation and decoding. Always add @idOf when your resolvers depend on typed IDs.
3. Mixing raw IDs with Global IDs
All ID arguments using @idOf are expected to be encoded Global IDs, not raw database identifiers. Passing
unencoded values will fail decoding or validation.
4. Misdeclaring the target type
Ensure the type name in @idOf(type: "X") matches the GraphQL type exactly, including case. "character" and
"Character" are not equivalent.
Do and don’t
- Do use
@idOfon everyIDfield or argument that carries a Global ID. - Do rely on
ctx.id.internalIDfor the decoded internal ID in resolvers. - Don’t attempt to parse or decode IDs manually.
- Don’t use
@idOfon non-ID fields.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.