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 DataFetchers 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 DataFetchers, 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

@Argument

For access to a named field argument bound to a higher-level, typed Object.

See @Argument.

@Argument Map<String, Object>

For access to the raw argument value.

See @Argument.

ArgumentValue

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 null.

See ArgumentValue.

@Arguments

For access to all field arguments bound to a higher-level, typed Object.

See @Arguments.

@Arguments Map<String, Object>

For access to the raw map of arguments.

@ProjectedPayload Interface

For access to field arguments through a project interface.

See @ProjectedPayload Interface.

"Source"

For access to the source (i.e. parent/container) instance of the field.

See Source.

Subrange and ScrollSubrange

For access to pagination arguments.

See Pagination, Scroll, Subrange.

Sort

For access to sort details.

See Pagination, Sort.

DataLoader

For access to a DataLoader in the DataLoaderRegistry.

See DataLoader.

@ContextValue

For access to an attribute from the main GraphQLContext in DataFetchingEnvironment.

@LocalContextValue

For access to an attribute from the local GraphQLContext in DataFetchingEnvironment.

GraphQLContext

For access to the context from the DataFetchingEnvironment.

java.security.Principal

Obtained from the Spring Security context, if available.

@AuthenticationPrincipal

For access to Authentication#getPrincipal() from the Spring Security context.

DataFetchingFieldSelectionSet

For access to the selection set for the query through the DataFetchingEnvironment.

Locale, Optional<Locale>

For access to the Locale from the DataFetchingEnvironment.

DataFetchingEnvironment

For direct access to the underlying DataFetchingEnvironment.

Return Values

Schema mapping handler methods can return:

  • A resolved value of any type.

  • Mono and Flux for asynchronous value(s). Supported for controller methods and for any DataFetcher as described in Reactive DataFetcher.

  • Kotlin coroutine and Flow are adapted to Mono and Flux.

  • java.util.concurrent.Callable to have the value(s) produced asynchronously. For this to work, AnnotatedControllerConfigurer must be configured with an Executor.

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 @BatchMapping handler method can batch load all authors for a query, given a list of source/parent books objects.

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, Book must implement hashcode and equals.

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

List<K>

The source/parent objects.

java.security.Principal

Obtained from Spring Security context, if available.

@ContextValue

For access to a value from the GraphQLContext of BatchLoaderEnvironment, which is the same context as the one from the DataFetchingEnvironment.

GraphQLContext

For access to the context from the BatchLoaderEnvironment, which is the same context as the one from the DataFetchingEnvironment.

BatchLoaderEnvironment

The environment that is available in GraphQL Java to a org.dataloader.BatchLoaderWithContext.

The context property of BatchLoaderEnvironment returns the same GraphQLContext instance that is also available to @SchemaMapping methods through the DataFetchingEnvironment.

The keyContexts property of BatchLoaderEnvironment returns the localContext obtained from the DataFetchingEnvironment when the DataLoader was called for each key.

Return Values

Batch mapping methods can return:

Return Type Description

Mono<Map<K,V>>

A map with parent objects as keys, and batch loaded objects as values.

Flux<V>

A sequence of batch loaded objects that must be in the same order as the source/parent objects passed into the method.

Map<K,V>, Collection<V>

Imperative variants, e.g. without remote calls to make.

Callable<Map<K,V>>, Callable<Collection<V>>

Imperative variants to be invoked asynchronously. For this to work, AnnotatedControllerConfigurer must be configured with an Executor.

Kotlin Coroutine with Map<K,V>, Kotlin Flow<K,V>

Adapted to Mono<Map<K,V> and Flux<V>.

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

graphql.GraphQLError

Resolve the exception to a single field error.

Collection<GraphQLError>

Resolve the exception to multiple field errors.

void

Resolve the exception without response errors.

Object

Resolve the exception to a single error, to multiple errors, or none. The return value must be GraphQLError, Collection<GraphQLError>, or null.

Mono<T>

For asynchronous resolution where <T> is one of the supported, synchronous, return types.

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