Annotated Controllers
Spring for GraphQL provides an annotation-based programming model where @Controller
components use annotations to declare handler methods with flexible method signatures to
fetch the data for specific GraphQL fields. For example:
@Controller
public class GreetingController {
@QueryMapping (1)
public String hello() { (2)
return "Hello, world!";
}
}
1 | Bind this method to a query, i.e. a field under the Query type. |
2 | Determine the query from the method name if not declared on the annotation. |
Spring for GraphQL uses RuntimeWiring.Builder
to register the above handler method as a
graphql.schema.DataFetcher
for the query named "hello".
Declaration
You can define @Controller
beans as standard Spring bean definitions. The
@Controller
stereotype allows for auto-detection, aligned with Spring general
support for detecting @Controller
and @Component
classes on the classpath and
auto-registering bean definitions for them. It also acts as a stereotype for the annotated
class, indicating its role as a data fetching component in a GraphQL application.
AnnotatedControllerConfigurer
detects @Controller
beans and registers their
annotated handler methods as DataFetcher
s via RuntimeWiring.Builder
. It is an
implementation of RuntimeWiringConfigurer
which can be added to GraphQlSource.Builder
.
The Boot Starter automatically declares AnnotatedControllerConfigurer
as a bean
and adds all RuntimeWiringConfigurer
beans to GraphQlSource.Builder
and that enables
support for annotated DataFetcher
s, see the
GraphQL RuntimeWiring section
in the Boot starter documentation.
@SchemaMapping
The @SchemaMapping
annotation maps a handler method to a field in the GraphQL schema
and declares it to be the DataFetcher
for that field. The annotation can specify the
parent type name, and the field name:
@Controller
public class BookController {
@SchemaMapping(typeName="Book", field="author")
public Author getAuthor(Book book) {
// ...
}
}
The @SchemaMapping
annotation can also leave out those attributes, in which case the
field name defaults to the method name, while the type name defaults to the simple class
name of the source/parent object injected into the method. For example, the below
defaults to type "Book" and field "author":
@Controller
public class BookController {
@SchemaMapping
public Author author(Book book) {
// ...
}
}
The @SchemaMapping
annotation can be declared at the class level to specify a default
type name for all handler methods in the class.
@Controller
@SchemaMapping(typeName="Book")
public class BookController {
// @SchemaMapping methods for fields of the "Book" type
}
@QueryMapping
, @MutationMapping
, and @SubscriptionMapping
are meta annotations that
are themselves annotated with @SchemaMapping
and have the typeName preset to Query
,
Mutation
, or Subscription
respectively. Effectively, these are shortcut annotations
for fields under the Query, Mutation, and Subscription types respectively. For example:
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInput bookInput) {
// ...
}
@SubscriptionMapping
public Flux<Book> newPublications() {
// ...
}
}
@SchemaMapping
handler methods have flexible signatures and can choose from a range of
method arguments and return values..
Method Arguments
Schema mapping handler methods can have any of the following method arguments:
Method Argument | Description |
---|---|
|
For access to a named field argument bound to a higher-level, typed Object. See |
|
For access to the raw argument value. See |
|
For access to a named field argument bound to a higher-level, typed Object along
with a flag to indicate if the input argument was omitted vs set to See |
|
For access to all field arguments bound to a higher-level, typed Object. See |
|
For access to the raw map of arguments. |
|
For access to field arguments through a project interface. |
"Source" |
For access to the source (i.e. parent/container) instance of the field. See Source. |
|
For access to pagination arguments. See Pagination, Scroll, |
|
For access to sort details. See Pagination, |
|
For access to a See |
|
For access to an attribute from the main |
|
For access to an attribute from the local |
|
For access to the context from the |
|
Obtained from the Spring Security context, if available. |
|
For access to |
|
For access to the selection set for the query through the |
|
For access to the |
|
For direct access to the underlying |
Return Values
Schema mapping handler methods can return:
-
A resolved value of any type.
-
Mono
andFlux
for asynchronous value(s). Supported for controller methods and for anyDataFetcher
as described in ReactiveDataFetcher
. -
Kotlin coroutine and
Flow
are adapted toMono
andFlux
. -
java.util.concurrent.Callable
to have the value(s) produced asynchronously. For this to work,AnnotatedControllerConfigurer
must be configured with anExecutor
.
On Java 21+, when AnnotatedControllerConfigurer
is configured with an Executor
, controller
methods with a blocking method signature are invoked asynchronously. By default, a controller
method is considered blocking if it does not return an async type such as Flux
, Mono
,
CompletableFuture
, and is also not a Kotlin suspending function. You can configure a
blocking controller method Predicate
on AnnotatedControllerConfigurer
to help
determine which methods are considered blocking.
The Spring Boot starter for Spring for GraphQL automatically configures
AnnotatedControllerConfigurer with an Executor for virtual threads when the property
spring.threads.virtual.enabled is set.
|
Interface Schema Mappings
When a controller method is mapped to a schema interface field, by default the mapping is replaced with multiple mappings, one for each schema object type that implements the interface. This allows use of one controller method for all subtypes.
For example, given:
type Query {
activities: [Activity!]!
}
interface Activity {
id: ID!
coordinator: User!
}
type FooActivity implements Activity {
id: ID!
coordinator: User!
}
type BarActivity implements Activity {
id: ID!
coordinator: User!
}
type User {
name: String!
}
You can write a controller like this:
@Controller
public class BookController {
@QueryMapping
public List<Activity> activities() {
// ...
}
@SchemaMapping
public User coordinator(Activity activity) {
// Called for any Activity subtype
}
}
If necessary, you can take over the mapping for individual subtypes:
@Controller
public class BookController {
@QueryMapping
public List<Activity> activities() {
// ...
}
@SchemaMapping
public User coordinator(Activity activity) {
// Called for any Activity subtype except FooActivity
}
@SchemaMapping
public User coordinator(FooActivity activity) {
// ...
}
}
@Argument
In GraphQL Java, DataFetchingEnvironment
provides access to a map of field-specific
argument values. The values can be simple scalar values (e.g. String, Long), a Map
of
values for more complex input, or a List
of values.
Use the @Argument
annotation to have an argument bound to a target object and
injected into the handler method. Binding is performed by mapping argument values to a
primary data constructor of the expected method parameter type, or by using a default
constructor to create the object and then map argument values to its properties. This is
repeated recursively, using all nested argument values and creating nested target objects
accordingly. For example:
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInput bookInput) {
// ...
}
}
If the target object doesn’t have setters, and you can’t change that, you can use a
property on AnnotatedControllerConfigurer to allow falling back on binding via direct
field access.
|
By default, if the method parameter name is available (requires the -parameters
compiler
flag with Java 8+ or debugging info from the compiler), it is used to look up the argument.
If needed, you can customize the name through the annotation, e.g. @Argument("bookInput")
.
The @Argument annotation does not have a "required" flag, nor the option to
specify a default value. Both of these can be specified at the GraphQL schema level and
are enforced by GraphQL Java.
|
If binding fails, a BindException
is raised with binding issues accumulated as field
errors where the field
of each error is the argument path where the issue occurred.
You can use @Argument
with a Map<String, Object>
argument, to obtain the raw value of
the argument. For example:
@Controller
public class BookController {
@MutationMapping
public Book addBook(@Argument Map<String, Object> bookInput) {
// ...
}
}
Prior to 1.2, @Argument Map<String, Object> returned the full arguments map if
the annotation did not specify a name. After 1.2, @Argument with
Map<String, Object> always returns the raw argument value, matching either to the name
specified in the annotation, or to the parameter name. For access to the full arguments
map, please use @Arguments instead.
|
ArgumentValue
By default, input arguments in GraphQL are nullable and optional, which means an argument
can be set to the null
literal, or not provided at all. This distinction is useful for
partial updates with a mutation where the underlying data may also be, either set to
null
or not changed at all accordingly. When using @Argument
there is no way to make such a distinction, because you would get null
or an empty
Optional
in both cases.
If you want to know not whether a value was not provided at all, you can declare an
ArgumentValue
method parameter, which is a simple container for the resulting value,
along with a flag to indicate whether the input argument was omitted altogether. You
can use this instead of @Argument
, in which case the argument name is determined from
the method parameter name, or together with @Argument
to specify the argument name.
For example:
@Controller
public class BookController {
@MutationMapping
public void addBook(ArgumentValue<BookInput> bookInput) {
if (!bookInput.isOmitted()) {
BookInput value = bookInput.value();
// ...
}
}
}
ArgumentValue
is also supported as a field within the object structure of an @Argument
method parameter, either initialized via a constructor argument or via a setter, including
as a field of an object nested at any level below the top level object.
@Arguments
Use the @Arguments
annotation, if you want to bind the full arguments map onto a single
target Object, in contrast to @Argument
, which binds a specific, named argument.
For example, @Argument BookInput bookInput
uses the value of the argument "bookInput"
to initialize BookInput
, while @Arguments
uses the full arguments map and in that
case, top-level arguments are bound to BookInput
properties.
You can use @Arguments
with a Map<String, Object>
argument, to obtain the raw map of
all argument values.
@ProjectedPayload
Interface
As an alternative to using complete Objects with @Argument
,
you can also use a projection interface to access GraphQL request arguments through a
well-defined, minimal interface. Argument projections are provided by
Spring Data’s Interface projections
when Spring Data is on the class path.
To make use of this, create an interface annotated with @ProjectedPayload
and declare
it as a controller method parameter. If the parameter is annotated with @Argument
,
it applies to an individual argument within the DataFetchingEnvironment.getArguments()
map. When declared without @Argument
, the projection works on top-level arguments in
the complete arguments map.
For example:
@Controller
public class BookController {
@QueryMapping
public Book bookById(BookIdProjection bookId) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInputProjection bookInput) {
// ...
}
}
@ProjectedPayload
interface BookIdProjection {
Long getId();
}
@ProjectedPayload
interface BookInputProjection {
String getName();
@Value("#{target.author + ' ' + target.name}")
String getAuthorAndName();
}
Source
In GraphQL Java, the DataFetchingEnvironment
provides access to the source (i.e.
parent/container) instance of the field. To access this, simply declare a method parameter
of the expected target type.
@Controller
public class BookController {
@SchemaMapping
public Author author(Book book) {
// ...
}
}
The source method argument also helps to determine the type name for the mapping.
If the simple name of the Java class matches the GraphQL type, then there is no need to
explicitly specify the type name in the @SchemaMapping
annotation.
A |
Subrange
When there is a CursorStrategy
bean in Spring configuration,
controller methods support a Subrange<P>
argument where <P>
is a relative position
converted from a cursor. For Spring Data, ScrollSubrange
exposes ScrollPosition
.
For example:
@Controller
public class BookController {
@QueryMapping
public Window<Book> books(ScrollSubrange subrange) {
ScrollPosition position = subrange.position().orElse(ScrollPosition.offset());
int count = subrange.count().orElse(20);
// ...
}
}
See Pagination for an overview of pagination and of built-in mechanisms.
Sort
When there is a SortStrategy bean in Spring configuration, controller
methods support Sort
as a method argument. For example:
@Controller
public class BookController {
@QueryMapping
public Window<Book> books(Optional<Sort> optionalSort) {
Sort sort = optionalSort.orElse(Sort.by(..));
}
}
DataLoader
When you register a batch loading function for an entity, as explained in
Batch Loading, you can access the DataLoader
for the entity by declaring a
method argument of type DataLoader
and use it to load the entity:
@Controller
public class BookController {
public BookController(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Map<Long, Author>
});
}
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
return loader.load(book.getAuthorId());
}
}
By default, BatchLoaderRegistry
uses the full class name of the value type (e.g. the
class name for Author
) for the key of the registration, and therefore simply declaring
the DataLoader
method argument with generic types provides enough information
to locate it in the DataLoaderRegistry
. As a fallback, the DataLoader
method argument
resolver will also try the method argument name as the key but typically that should not
be necessary.
Note that for many cases with loading related entities, where the @SchemaMapping
simply
delegates to a DataLoader
, you can reduce boilerplate by using a
@BatchMapping method as described in the next section.
Validation
When a javax.validation.Validator
bean is found, AnnotatedControllerConfigurer
enables support for
Bean Validation
on annotated controller methods. Typically, the bean is of type LocalValidatorFactoryBean
.
Bean validation lets you declare constraints on types:
public class BookInput {
@NotNull
private String title;
@NotNull
@Size(max=13)
private String isbn;
}
You can then annotate a controller method parameter with @Valid
to validate it before
method invocation:
@Controller
public class BookController {
@MutationMapping
public Book addBook(@Argument @Valid BookInput bookInput) {
// ...
}
}
If an error occurs during validation, a ConstraintViolationException
is raised.
You can use the Exceptions chain to decide how to present that to clients
by turning it into an error to include in the GraphQL response.
In addition to @Valid , you can also use Spring’s @Validated that allows
specifying validation groups.
|
Bean validation is useful for @Argument
,
@Arguments
, and
@ProjectedPayload
method parameters, but applies more generally to any method parameter.
Validation and Kotlin Coroutines
Hibernate Validator is not compatible with Kotlin Coroutine methods and fails when introspecting their method parameters. Please see spring-projects/spring-graphql#344 (comment) for links to relevant issues and a suggested workaround. |
@BatchMapping
Batch Loading addresses the N+1 select problem through the use of an
org.dataloader.DataLoader
to defer the loading of individual entity instances, so they
can be loaded together. For example:
@Controller
public class BookController {
public BookController(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Map<Long, Author>
});
}
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
return loader.load(book.getAuthorId());
}
}
For the straight-forward case of loading an associated entity, shown above, the
@SchemaMapping
method does nothing more than delegate to the DataLoader
. This is
boilerplate that can be avoided with a @BatchMapping
method. For example:
@Controller
public class BookController {
@BatchMapping
public Mono<Map<Book, Author>> author(List<Book> books) {
// ...
}
}
The above becomes a batch loading function in the BatchLoaderRegistry
where keys are Book
instances and the loaded values their authors. In addition, a
DataFetcher
is also transparently bound to the author
field of the type Book
, which
simply delegates to the DataLoader
for authors, given its source/parent Book
instance.
To be used as a unique key, |
By default, the field name defaults to the method name, while the type name defaults to
the simple class name of the input List
element type. Both can be customized through
annotation attributes. The type name can also be inherited from a class level
@SchemaMapping
.
Method Arguments
Batch mapping methods support the following arguments:
Method Argument | Description |
---|---|
|
The source/parent objects. |
|
Obtained from Spring Security context, if available. |
|
For access to a value from the |
|
For access to the context from the |
|
The environment that is available in GraphQL Java to a
The The |
Return Values
Batch mapping methods can return:
Return Type | Description |
---|---|
|
A map with parent objects as keys, and batch loaded objects as values. |
|
A sequence of batch loaded objects that must be in the same order as the source/parent objects passed into the method. |
|
Imperative variants, e.g. without remote calls to make. |
|
Imperative variants to be invoked asynchronously. For this to work,
|
Kotlin Coroutine with |
Adapted to |
On Java 21+, when AnnotatedControllerConfigurer
is configured with an Executor
, controller
methods with a blocking method signature are invoked asynchronously. By default, a controller
method is considered blocking if it does not return an async type such as Flux
, Mono
,
CompletableFuture
, and is also not a Kotlin suspending function. You can configure a
blocking controller method Predicate
on AnnotatedControllerConfigurer
to help
determine which methods are considered blocking.
The Spring Boot starter for Spring for GraphQL automatically configures
AnnotatedControllerConfigurer with an Executor for virtual threads when the property
spring.threads.virtual.enabled is set.
|
Interface Batch Mappings
As is the case with Interface Schema Mappings, when a batch mapping method is mapped to a schema interface field, the mapping is replaced with multiple mappings, one for each schema object type that implements the interface.
That means, given the following:
type Query {
activities: [Activity!]!
}
interface Activity {
id: ID!
coordinator: User!
}
type FooActivity implements Activity {
id: ID!
coordinator: User!
}
type BarActivity implements Activity {
id: ID!
coordinator: User!
}
type User {
name: String!
}
You can write a controller like this:
@Controller
public class BookController {
@QueryMapping
public List<Activity> activities() {
// ...
}
@BatchMapping
Map<Activity, User> coordinator(List<Activity> activities) {
// Called for all Activity subtypes
}
}
If necessary, you can take over the mapping for individual subtypes:
@Controller
public class BookController {
@QueryMapping
public List<Activity> activities() {
// ...
}
@BatchMapping
Map<Activity, User> coordinator(List<Activity> activities) {
// Called for all Activity subtypes
}
@BatchMapping(field = "coordinator")
Map<Activity, User> fooCoordinator(List<FooActivity> activities) {
// ...
}
}
@GraphQlExceptionHandler
Use @GraphQlExceptionHandler
methods to handle exceptions from data fetching with a
flexible method signature. When declared in a
controller, exception handler methods apply to exceptions from the same controller:
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@GraphQlExceptionHandler
public GraphQLError handle(BindException ex) {
return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build();
}
}
When declared in an @ControllerAdvice
, exception handler methods apply across controllers:
@ControllerAdvice
public class GlobalExceptionHandler {
@GraphQlExceptionHandler
public GraphQLError handle(BindException ex) {
return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build();
}
}
Exception handling via @GraphQlExceptionHandler
methods is applied automatically to
controller invocations. To handle exceptions from other graphql.schema.DataFetcher
implementations, not based on controller methods, obtain a
DataFetcherExceptionResolver
from AnnotatedControllerConfigurer
, and register it in
GraphQlSource.Builder
as a DataFetcherExceptionResolver.
Method Signature
Exception handler methods support a flexible method signature with method arguments
resolved from a DataFetchingEnvironment,
and matching to those of
@SchemaMapping methods.
Supported return types are listed below:
Return Type | Description |
---|---|
|
Resolve the exception to a single field error. |
|
Resolve the exception to multiple field errors. |
|
Resolve the exception without response errors. |
|
Resolve the exception to a single error, to multiple errors, or none.
The return value must be |
|
For asynchronous resolution where |
Namespacing
At the schema level, query and mutation operations are defined directly under the Query
and Mutation
types.
Rich GraphQL APIs can define dozens of operation sunder those types, making it harder to explore the API and separate concerns.
You can choose to define Namespaces in your GraphQL schema.
While there are some caveats with this approach, you can implement this pattern with Spring for GraphQL annotated controllers.
With namespacing, your GraphQL schema can, for example, nest query operations under top-level types, instead of listing them directly under Query
.
Here, we will define MusicQueries
and UserQueries
types and make them available under Query
:
type Query {
music: MusicQueries
users: UserQueries
}
type MusicQueries {
album(id: ID!): Album
searchForArtist(name: String!): [Artist]
}
type Album {
id: ID!
title: String!
}
type Artist {
id: ID!
name: String!
}
type UserQueries {
user(login: String): User
}
type User {
id: ID!
login: String!
}
A GraphQL client would use the album
query like this:
{
music {
album(id: 42) {
id
title
}
}
}
And get the following response:
{
"data": {
"music": {
"album": {
"id": "42",
"title": "Spring for GraphQL"
}
}
}
}
This can be implemented in a @Controller
with the following pattern:
import java.util.List;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
@Controller
@SchemaMapping(typeName = "MusicQueries") (1)
public class MusicController {
@QueryMapping (2)
public MusicQueries music() {
return new MusicQueries();
}
(3)
public record MusicQueries() {
}
@SchemaMapping (4)
public Album album(@Argument String id) {
return new Album(id, "Spring GraphQL");
}
@SchemaMapping
public List<Artist> searchForArtist(@Argument String name) {
return List.of(new Artist("100", "the Spring team"));
}
}
1 | Annotate the controller with @SchemaMapping and a typeName attribute, to avoid repeating it on methods |
2 | Define a @QueryMapping for the "music" namespace |
3 | The "music" query returns an "empty" record, but could also return an empty map |
4 | Queries are now declared as fields under the "MusicQueries" type |
Instead of declaring wrapping types ("MusicQueries", "UserQueries") explicitly in controllers,
you can choose to configure them with the runtime wiring using a GraphQlSourceBuilderCustomizer
with Spring Boot:
import java.util.Collections;
import java.util.List;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class NamespaceConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer customizer() {
List<String> queryWrappers = List.of("music", "users"); (1)
return (sourceBuilder) -> sourceBuilder.configureRuntimeWiring((wiringBuilder) ->
queryWrappers.forEach((field) -> wiringBuilder.type("Query",
(builder) -> builder.dataFetcher(field, (env) -> Collections.emptyMap()))) (2)
);
}
}
1 | List all the wrapper types for the "Query" type |
2 | Manually declare data fetchers for each of them, returning an empty Map |