Spring Boot: Annotated Controllers

This section documents the current GraphQL specification (October 2021), plus the working draft for @defer/@stream/@oneOf, and the GraphQL-over-HTTP specification, as published at graphql.org and spec.graphql.org — which are the references these pages are written and verified against — and, for the integration pages, against Spring for GraphQL (2.0.x), Strawberry, Ariadne, and the client docs for Apollo Client / urql / Relay. No specific patch version is pinned. Some surfaces (Apollo Router/managed federation, graphql-ws internals, the Relay compiler internals, GraalVM native) are linked rather than documented in depth.

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

@ContextValue

An attribute from the request’s main GraphQLContext (shared across the whole query).

@LocalContextValue

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 Execution & resolvers).

GraphQLContext

The context object itself, for reading or writing several attributes at once.

java.security.Principal

The current principal from the Spring Security context, if a security context is present.

@AuthenticationPrincipal

The value of Authentication#getPrincipal() from the Spring Security context.

DataFetchingFieldSelectionSet

The client’s selection set under the current field, useful for deciding how much related data to fetch.

Locale / Optional<Locale>

The locale associated with the current DataFetchingEnvironment.

@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

T

Any plain application type, resolved synchronously and used as-is.

Mono<T> / Flux<T>

Resolved asynchronously through Project Reactor; Flux<T> is what backs @SubscriptionMapping streams (see Subscriptions). Also supported directly on a reactive DataFetcher, not only controller methods.

Kotlin suspend fun / Flow<T>

Adapted transparently to Mono<T> / Flux<T>.

java.util.concurrent.Callable<T>

Produces the value asynchronously on an Executor configured on AnnotatedControllerConfigurer. On Java 21+ with virtual threads, returning T directly from a method that blocks is often enough instead.

graphql.execution.DataFetcherResult<P>

The "full" graphql-java result type, wrapping any of the above (P can itself be T, Mono<T>, and so on) plus response extensions or per-field errors alongside the data.

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.