Field Resolvers
Schema
All schema fields with the @resolver
directive have a corresponding field resolver. This directive can only be placed on object, not interface fields.
In this example schema, we’ve added @resolver to the displayName field:
type User implements Node {
id: ID!
firstName: String
lastName: String
displayName: String @resolver
}
When to use @resolver
Field resolvers are typically used in the following scenarios:
-
Fields with arguments should have their own resolver, since resolvers don’t have access to the arguments of nested fields:
address(format: AddressFormat): Address @resolver -
Fields that are backed by a different data source than the core fields on a type should have their own resolver. In the example below, suppose the resolver for
wishlistsis backed by a Wishlist service endpoint, whereasfirstNameandlastNameare backed by a User service endpoint:firstName: String lastName: String wishlists: [Wishlist] @resolverThis avoids executing the
wishlistsresolver and calling the Wishlist service if the field isn’t in the client query. -
Fields that are derived from other fields, such as the
displayNameexample shown in more detail below, which is derived fromfirstNameandlastName. Although this example is simple, in practice there can be complex resolvers that have large required selection sets. This keeps the logic for these fields contained in their own resolvers which is easier to understand and maintain.
Generated base class
Viaduct generates an abstract base class for all schema fields with the @resolver directive. For User.displayName, Viaduct generates the following code:
object UserResolvers {
abstract class DisplayName {
open suspend fun resolve(ctx: Context): String? =
throw NotImplementedError()
open suspend fun batchResolve(contexts: List<Context>): List<FieldValue<String?>> =
throw NotImplementedError()
class Context: FieldExecutionContext<User, Query, NoArguments, NotComposite>
}
// If there were more User fields with @resolver, their base classes would be generated here
}
The nested Context class is described in more detail below.
Implementation
Implement a field resolver by subclassing the generated base class, and overriding exactly one of either resolve or batchResolve. Learn more about batch resolution here.
Let’s look at the resolver for User.displayName:
@Resolver(
"fragment _ on User { firstName lastName }"
)
class UserDisplayNameResolver : UserResolvers.DisplayName() {
override suspend fun resolve(ctx: Context): String? {
val fn = ctx.objectValue.getFirstName()
val ln = ctx.objectValue.getLastName()
return when {
fn == null && ln == null -> null
fn == null -> ln
ln == null -> fn
else -> "$fn $ln"
}
}
}
As this example illustrates, the @Resolver annotation can contain an optional fragment on the parent type of the field being resolved. We call this fragment the required selection set of the resolver. In this case, the required selection set asks for the firstName and lastName fields of User, which are combined to generate the user’s display name. If a resolver attempts to access a field that’s not in its required selection set, an UnsetSelectionException is thrown at runtime.
The @Resolver annotation can also be used to declare data dependencies on the root Query type. Learn more about the annotation here.
Important clarification: there are no requirements on the names of these resolver classes: We use UserDisplayNameResolver here as an example of a typical name, but that choice is not dictated by the framework.
Context
Both resolve and batchResolve take Context objects as input. This class is an instance of FieldExecutionContext
:
/**
* An [ExecutionContext] provided to field resolvers
*/
interface FieldExecutionContext<T : Object, Q : Query, A : Arguments, O : CompositeOutput> : ResolverExecutionContext {
/**
* A value of [T], with any (and only) selections from [viaduct.api.Resolver.objectValueFragment]
* populated.
* Attempting to access fields not declared in [viaduct.api.Resolver.objectValueFragment] will
* throw a runtime exception
*/
val objectValue: T
/**
* A value of [Q], with any (and only) selections from [viaduct.api.Resolver.queryValueFragment]
* populated.
* Attempting to access fields not declared in [viaduct.api.Resolver.queryValueFragment] will
* throw a runtime exception
*/
val queryValue: Q
/**
* The value of any [A] arguments that were provided by the caller of this
* resolver. If this field does not take arguments, this is [Arguments.NoArguments].
*/
val arguments: A
/**
* The [SelectionSet] for [O] that the caller provided. If this field does not have a
* selection set (i.e. it has a scalar or enum type), this returns [SelectionSet.NoSelections].
*/
fun selections(): SelectionSet<O>
}
-
objectValuegives access to the object that contains the field being resolved. Fields of that object can be accessed, but only if those fields are in the resolver’s required selection set. If the resolver tries to access a field not included within its required selection set, it results in anUnsetSelectionExceptionat runtime. -
queryValueis similar toobjectValue, but applies to the root query object of the Viaduct central schema. LikeobjectValue, fields onqueryValuecan only be accessed if they are in the resolver’s required selection set. -
argumentsgives access to the arguments to the resolver. When a field takes arguments, the Viaduct build system will generate a GRT representing the values of those arguments. IfUser.displayNametook arguments, for example, Viaduct would generate a typeUser_DisplayName_Argumentshaving one property per argument taken bydisplayName. In our example, the field execution context fordisplayNameis parameterized by the special typeNoArgumentsindicating that the field takes no arguments. -
selections()returns the selections being requested for this field in the query, same as theselectionsfunction for the node resolver. TheSelectionSettype is parameterized by the type of the selection set. For example, in the case ofUser’s node resolver,selectionsreturnedSelectionSet<User>. In the case ofdisplayName,selectionsreturnsSelectionSet<NotComposite>, where the special typeNotCompositeindicates thatdisplayNamedoes not return a composite type (it returns a scalar instead).
Since NodeExecutionContext
implements ResolverExecutionContext
, it also includes the utilities provided there, which allow you to:
- Execute subqueries
- Construct node references
- Construct GlobalIDs
Responsibility set
For scalar and enum fields like displayName, the field resolver is just responsible for resolving the single field. If the field has a node type, the field resolver constructs a node reference using just the node’s GlobalID, which tells the engine to run the node resolver. For fields with non-node object types, the field resolver is responsible for all nested fields without its own resolver.
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.