Fields
Fields for object and interface types are declared on the builder. Each field carries its resolver, arguments, GraphQL type and options in one place. The same options apply to root fields, which omit the model argument.
Scalar fields
The simplest field reads a property:
Or computes a value with a resolver:
There are no separate scalar helper methods. The GraphQL type comes from the resolver's Rust
return type, so a String is String!, an i32 is Int!, and so on. The compiler checks the
type instead of a string type name.
Resolver shapes
Object fields receive the parent model, their arguments and the request context. Four shapes cover sync/async and fallible/infallible resolvers:
Each has a *_with_info variant that appends &gql::ResolveInfo<'_>:
Root fields use the same methods without the model argument:
field_with_execution / field_result_with_execution append &gql::ResolverContext<'_>, which
exposes the execution context for response extensions:
Lists
Lists come from Rust collections. Vec<T> becomes [T]!; the item type keeps its own
nullability:
Use field options when the Rust type is truthful but the API needs a different wrapper:
Overrides are checked at build time. A nullable Rust position can never be made non-null, and a list shape must match the Rust collection. See Nullability for the full rules.
Arguments
A resolver's second parameter is its arguments type. Derive Args and the GraphQL arguments
follow automatically:
Args produces arguments on the field; it does not register a named input type. See Using
args and Input objects.
Exposing fields from the model
expose! reads a property and infers both the field name and the GraphQL type:
The macro requires an object model; it is not available on root types. For a computed or transformed value, use a resolver.
The synchronous property path
expose! installs a synchronous property resolver, not an async resolver. The executor calls it
inline on both the leaf fast path and the general completion path, so reading an exposed property
never allocates or polls a future. A query over exposed scalar properties therefore pays only for
the value conversion (String clones allocate; i32, f64, bool and ID do not), not for the
resolver.
Descriptors and metadata never leave this path: description, deprecated, complexity,
directive, as_type/returns, nullable/non_null, nullable_items/non_null_items,
as_abstract, as_object_ref and as_dynamic only change the field definition. So does a plugin
whose field hook changes metadata but whose wrap_resolve returns next.
A boxed async resolver is installed only when an option needs a future to run:
Object- and interface-level authorization also boxes the fields it reaches, because it wraps each
field's resolver. The conversion happens once, at build time, so only the affected fields lose the
synchronous path. raw_field stays resolver-free: it has neither kind of resolver, and its value
comes from the raw object map.
Field options
field, expose and the root variants return FieldConfig. Options apply when the statement
ends:
FieldConfig also has:
Type-level authorization is declared on the object or interface and inherited by every field. See Scope auth.
Adding fields to an existing type
object, object_fields and object_field can be called repeatedly:
The same pattern works for interface / interface_fields and the root builders. This is how
a large type is split across modules.
Nested lists
Rust expresses nested lists directly:
To make inner items or lists nullable, use Option in the collection type or the field options
where the Rust type already permits it.
