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
wishlists
is backed by a Wishlist service endpoint, whereasfirstName
andlastName
are backed by a User service endpoint:firstName: String lastName: String wishlists: [Wishlist] @resolver
This avoids executing the
wishlists
resolver and calling the Wishlist service if the field isn’t in the client query. -
Fields that are derived from other fields, such as the
displayName
example shown in more detail below, which is derived fromfirstName
andlastName
. 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
:
interface FieldExecutionContext<T: Object, Q: Query, A: Arguments, O: CompositeOutput>: ResolverExecutionContext {
val objectValue: T
val queryValue: Q
val arguments: A
fun selections(): SelectionSet<O>
}
-
objectValue
gives 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 anUnsetSelectionException
at runtime. -
queryValue
is similar toobjectValue
, but applies to the root query object of the Viaduct central schema. LikeobjectValue
, fields onqueryValue
can only be accessed if they are in the resolver’s required selection set. -
arguments
gives 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.displayName
took arguments, for example, Viaduct would generate a typeUser_DisplayName_Arguments
having one property per argument taken bydisplayName
. In our example, the field execution context fordisplayName
is parameterized by the special typeNoArguments
indicating that the field takes no arguments. -
selections()
returns the selections being requested for this field in the query, same as theselections
function for the node resolver. TheSelectionSet
type is parameterized by the type of the selection set. For example, in the case ofUser
’s node resolver,selections
returnedSelectionSet<User>
. In the case ofdisplayName
,selections
returnsSelectionSet<NotComposite>
, where the special typeNotComposite
indicates thatdisplayName
does 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.