Spring Boot: Annotated Controllers
|
This section documents the current GraphQL specification (October 2021), plus the working draft for
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as GraphQL tooling iterates quickly. |
Spring for GraphQL’s annotated controller model is the primary way to wire Java methods up as GraphQL
DataFetcher s. This page covers declaring a @Controller, mapping methods to schema fields, binding
arguments, and the return types a handler method can produce.
@Controller and @SchemaMapping
A @Controller bean is detected the same way as any other Spring stereotype — component scanning picks it up
like @Service or @Repository — but it also signals to AnnotatedControllerConfigurer that the bean’s
annotated methods should be registered as DataFetcher s against the schema’s RuntimeWiring. The Spring
Boot starter auto-configures AnnotatedControllerConfigurer as a bean, so no manual wiring is needed; see
Spring Boot: Getting started for the starter dependency
and auto-configuration this relies on.
@SchemaMapping is the base annotation: it maps a handler method to a field on a named type and makes that
method the field’s DataFetcher. Both the type name and field name can be given explicitly, or left to be
inferred:
@Controller
public class BookController {
// Explicit: Book.author
@SchemaMapping(typeName = "Book", field = "author")
public Author getAuthor(Book book) {
return authorService.findById(book.authorId());
}
// Inferred: field name defaults to the method name ("author"), type name
// defaults to the simple class name of the injected source object ("Book")
@SchemaMapping
public Author author(Book book) {
return authorService.findById(book.authorId());
}
}
// A class-level @SchemaMapping sets the default type name for every method in the class
@Controller
@SchemaMapping(typeName = "Book")
public class BookFieldsController {
// methods below only need to name the field, not "Book" again
}
@QueryMapping, @MutationMapping, and @SubscriptionMapping are meta-annotations built on top of
@SchemaMapping with the type name preset to Query, Mutation, and Subscription respectively — they are
shorthands, not a separate mechanism:
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
return bookService.findById(id);
}
@MutationMapping
public Book addBook(@Argument BookInput bookInput) {
return bookService.create(bookInput);
}
@SubscriptionMapping
public Flux<Book> newPublications() {
return bookService.publicationStream();
}
}
See
Spring for GraphQL — Declaration for how @Controller beans are detected, and
Spring for GraphQL — \@SchemaMapping for the full annotation, including the class-level default shown above. Rich schemas that
group many query/mutation fields under intermediate types rather than listing them all directly under Query
can also use namespacing — see
Spring for GraphQL — Namespacing.
Binding arguments with @Argument
@Argument binds a single named field argument to a handler method parameter. If the target parameter is a
simple scalar type (String, Long, and so on) the raw value is passed through; if it is a complex type,
Spring binds it by invoking a constructor with the nested argument values (record classes are a natural fit
here), falling back to a default constructor plus setters, or direct field access when neither is available:
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
return bookService.findById(id);
}
@MutationMapping
public Book addBook(@Argument BookInput bookInput) {
return bookService.create(bookInput);
}
}
public record BookInput(String title, int publishedYear, Long authorId) {
}
By default the argument is looked up by the method parameter’s own name (which requires the -parameters
compiler flag, or debug info, to be preserved at compile time); @Argument("bookInput") overrides the lookup
name explicitly. @Argument without a value on the whole parameter binds the named argument the parameter
represents; declaring the parameter as Map<String, Object> instead gives access to the raw, unbound argument
value. A parameter annotated @Arguments (plural) binds the entire arguments map for the field onto one
target object, rather than a single named argument — useful when a mutation’s input fields are not grouped
under one wrapper argument in the schema. ArgumentValue<T> is a third option that additionally distinguishes
an argument the client omitted entirely from one explicitly set to null, which plain @Argument binding
cannot tell apart.
See Spring for GraphQL — \@Argument and Spring for GraphQL — Argument Binding for the full constructor/setter/field-access binding algorithm, and Spring for GraphQL — ArgumentValue for the omitted-vs-null distinction.
@ProjectedPayload argument interfaces
As an alternative to a concrete input class, a controller method can declare a parameter typed as an interface
annotated @ProjectedPayload. Spring Data’s interface projections then expose only the getters the interface
declares, backed by the request’s argument map, including derived properties computed with a SpEL expression:
@Controller
public class BookController {
// No @Argument: the projection reads the top-level arguments map directly
@QueryMapping
public Book bookById(BookIdProjection bookId) {
return bookService.findById(bookId.getId());
}
// @Argument: the projection reads one named argument's nested value
@MutationMapping
public Book addBook(@Argument BookInputProjection bookInput) {
return bookService.create(bookInput);
}
}
@ProjectedPayload
interface BookIdProjection {
Long getId();
}
@ProjectedPayload
interface BookInputProjection {
String getTitle();
@Value("#{target.title + ' (' + target.publishedYear + ')'}")
String getDisplayTitle();
}
This requires Spring Data’s interface-projection support on the classpath. It reads well for read-mostly, partial views over a larger input shape, where defining a whole record class for every combination of fields a client might send would be more ceremony than the mapping is worth. See Spring for GraphQL — \@ProjectedPayload Interface.
Context, security, and request-scoped values
A handler method can pull additional values out of the current DataFetchingEnvironment by declaring the
right parameter type, without threading a DataFetchingEnvironment parameter through by hand:
| Parameter type | What it provides |
|---|---|
|
An attribute from the request’s main |
|
An attribute from the local context of the current field (scoped to that part of the result tree, and set
by an ancestor field’s resolver — see |
|
The context object itself, for reading or writing several attributes at once. |
|
The current principal from the Spring Security context, if a security context is present. |
|
The value of |
|
The client’s selection set under the current field, useful for deciding how much related data to fetch. |
|
The locale associated with the current |
@Controller
public class BookController {
@SchemaMapping
public List<Book> related(Book book, @LocalContextValue Author author) {
return bookService.findByAuthor(author);
}
@MutationMapping
@PreAuthorize("hasRole('CURATOR')")
public Book addBook(@Argument BookInput bookInput, Principal principal) {
return bookService.create(bookInput, principal.getName());
}
}
Authorization annotations such as @PreAuthorize compose normally with @Controller handler methods once
Spring Security’s method security is active; see
Authorization and
Spring Boot: Transports, security, and
testing for the security-filter-chain side of wiring a GraphQL endpoint. For the full method-argument table
(including DataLoader, Source, Sort, and Subrange, covered below), see
Spring for
GraphQL — Method Arguments, and for local context specifically,
Spring
for GraphQL — Local Context.
Return values: plain values, Mono/Flux, and CompletableFuture
A handler method’s return type does not have to be the resolved value itself. Spring for GraphQL adapts several asynchronous and reactive return types automatically, so a controller can mix synchronous and asynchronous resolvers freely across the same schema:
| Return type | Behavior |
|---|---|
|
Any plain application type, resolved synchronously and used as-is. |
|
Resolved asynchronously through Project Reactor; |
Kotlin |
Adapted transparently to |
|
Produces the value asynchronously on an |
|
The "full" graphql-java result type, wrapping any of the above ( |
CompletableFuture<T> shows up constantly in practice as the return type of a method that loads through a
registered DataLoader — the batching mechanism that fixes the N+1 problem for per-item fields such as
Book.author:
@Controller
public class BookController {
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
return loader.load(book.authorId());
}
}
DataLoader<K, V> is itself a supported method-argument type, resolved from the request’s DataLoaderRegistry
by the key type registered for it. @BatchMapping — the higher-level annotation that registers the batch
loading function for a whole field instead of writing this by hand — and the Spring Data integration around it
are covered in depth in
Spring Boot: Data loading and integration;
for the underlying N+1 problem itself, see
Performance and the N+1 problem. See
Spring
for GraphQL — Return Values and
Spring
for GraphQL — DataLoader for the complete list and the executor configuration Callable<T> depends on.
Binding Sort and ScrollSubrange pagination arguments
Two more method-argument types exist specifically for paginated list fields, each gated by a corresponding strategy bean being present in the application context:
@Controller
public class BookController {
@QueryMapping
public Window<Book> books(Optional<Sort> sort, ScrollSubrange subrange) {
Sort effectiveSort = sort.orElse(Sort.by("title"));
ScrollPosition position = subrange.position().orElse(ScrollPosition.offset());
int count = subrange.count().orElse(20);
return bookService.findPage(effectiveSort, position, count);
}
}
Sort is bound automatically as a method parameter once a SortStrategy bean is configured, translating the
schema’s sort argument (however it is named and shaped there) into a Spring Data Sort. ScrollSubrange
behaves the same way once a CursorStrategy bean is configured, and is the Spring Data-flavored specialization
of the more general Subrange<P> argument, where P is the relative position decoded from a client-supplied
cursor — ScrollSubrange narrows P to Spring Data’s own ScrollPosition. Both types compose naturally with
the auto-registered QuerydslDataFetcher/QueryByExampleDataFetcher Spring Data integration described in
Spring Boot: Data loading and integration.
For the pagination model these arguments implement on the wire — offset vs. cursor pagination and the Relay
Cursor Connections shape — see Pagination.
Putting it together
A single controller commonly mixes several of the pieces above — synchronous and DataLoader-backed fields,
constructor-bound and projected arguments, and a context-scoped subscription field:
@Controller
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@QueryMapping
public Book bookById(@Argument Long id) {
return bookService.findById(id);
}
@QueryMapping
public Window<Book> books(Optional<Sort> sort, ScrollSubrange subrange) {
return bookService.findPage(sort.orElse(Sort.by("title")), subrange);
}
@MutationMapping
@PreAuthorize("hasRole('CURATOR')")
public Book addBook(@Argument BookInput bookInput, Principal principal) {
return bookService.create(bookInput, principal.getName());
}
@SubscriptionMapping
public Flux<Book> newPublications(@ContextValue(required = false) String genreFilter) {
return bookService.publicationStream(genreFilter);
}
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
return loader.load(book.authorId());
}
}
Each method here maps to exactly one schema field and is registered as that field’s DataFetcher the same way,
regardless of whether it returns a plain value, a Flux, or a CompletableFuture — the annotation and
argument-resolution model described on this page is what makes that uniform across query, mutation,
subscription, and per-field resolution. The next pages in this section build directly on it:
Spring Boot: Data loading and integration
covers @BatchMapping and the Spring Data auto-registration in depth, and
Spring Boot: Transports, security, and
testing covers the HTTP/WebSocket transport and security-filter-chain configuration these controllers run
behind.