1. Overview

Spring for GraphQL provides support for Spring applications built on GraphQL Java. It is a joint collaboration between both teams. Our shared philosophy is to be less opinionated and more focused on comprehensive and wide-ranging support.

Spring for GraphQL is the successor of the GraphQL Java Spring project from the GraphQL Java team. It aims to be the foundation for all Spring, GraphQL applications.

The project is in a milestone phase towards a 1.0 release, currently, and looking for feedback. Please, use our issue tracker to report a problem, discuss a design issue, or request a feature.

To get started, check the Spring GraphQL starter on start.spring.io and the Samples sections.

2. Requirements

Spring for GraphQL requires the following as a baseline:

  • JDK8

  • Spring Framework 5.3

  • GraphQL Java 17

  • Spring Data 2021.1.0 or later for QueryDSL or Query by Example

3. Web Transports

Spring for GraphQL supports GraphQL requests over HTTP and over WebSocket.

3.1. HTTP

GraphQlHttpHandler handles GraphQL over HTTP requests and delegates to the Web Interception chain for request execution. There are two variants, one for Spring MVC and one for Spring WebFlux. Both handle requests asynchronously and have equivalent functionality, but rely on blocking vs non-blocking I/O respectively for writing the HTTP response.

Requests must use HTTP POST with GraphQL request details included as JSON in the request body, as defined in the proposed GraphQL over HTTP specification. Once the JSON body has been successfully decoded, the HTTP response status is always 200 (OK), and any errors from GraphQL request execution appear in the "errors" section of the GraphQL response.

GraphQlHttpHandler can be exposed as an HTTP endpoint by declaring a RouterFunction bean and using the RouterFunctions from Spring MVC or WebFlux to create the route. The Boot starter does this, see the Web Endpoints section for details, or check GraphQlWebMvcAutoConfiguration or GraphQlWebFluxAutoConfiguration it contains, for the actual config.

The Spring for GraphQL repository contains a Spring MVC HTTP sample application.

3.2. WebSocket

GraphQlWebSocketHandler handles GraphQL over WebSocket requests based on the protocol defined in the graphql-ws library. The main reason to use GraphQL over WebSocket is subscriptions which allow sending a stream of GraphQL responses, but it can also be used for regular queries with a single response. The handler delegates every request to the Web Interception chain for further request execution.

GraphQL Over WebSocket Protocols

There are two such protocols, one in the subscriptions-transport-ws library and another in the graphql-ws library. The former is not active and succeeded by the latter. Read this blog post for the history.

There are two variants of GraphQlWebSocketHandler, one for Spring MVC and one for Spring WebFlux. Both handle requests asynchronously and have equivalent functionality. The WebFlux handler also uses non-blocking I/O and back pressure to stream messages, which works well since in GraphQL Java a subscription response is a Reactive Streams Publisher.

The graphql-ws project lists a number of recipes for client use.

GraphQlWebSocketHandler can be exposed as a WebSocket endpoint by declaring a SimpleUrlHandlerMapping bean and using it to map the handler to a URL path. The Boot starter has options to enable this, see the Web Endpoints section for details, or check GraphQlWebMvcAutoConfiguration or GraphQlWebFluxAutoConfiguration it contains, for the actual config.

The Spring for GraphQL repository contains a WebFlux WebSocket sample application.

3.3. Web Interception

HTTP and WebSocket transport handlers delegate to a common Web interception chain for request execution. The chain consists of a sequence of WebInterceptor components, followed by a GraphQlService that invokes the GraphQL Java engine.

WebInterceptor is as a common contract to use in both Spring MVC and WebFlux applications. Use it to intercept requests, inspect HTTP request headers, or to register a transformation of the graphql.ExecutionInput:

class MyInterceptor implements WebInterceptor {

    @Override
    public Mono<WebOutput> intercept(WebInput webInput, WebInterceptorChain chain) {
        webInput.configureExecutionInput((executionInput, builder) -> {
            Map<String, Object> map = ... ;
            return builder.extensions(map).build();
        });
        return chain.next(webInput);
    }
}

Use WebInterceptor also to intercept responses, add HTTP response headers, or transform the graphql.ExecutionResult:

class MyInterceptor implements WebInterceptor {

    @Override
    public Mono<WebOutput> intercept(WebInput webInput, WebInterceptorChain chain) {
        return chain.next(webInput)
                .map(webOutput -> {
                    Object data = webOutput.getData();
                    Object updatedData = ... ;
                    return webOutput.transform(builder -> builder.data(updatedData));
                });
    }
}

WebGraphQlHandler provides a builder to initialize the Web interception chain. After you build the chain, you can use the resulting WebGraphQlHandler to initialize the HTTP or WebSocket transport handlers. The Boot starter configures all this, see the Web Endpoints section for details, or check GraphQlWebMvcAutoConfiguration or GraphQlWebFluxAutoConfiguration it contains, for the actual config.

4. Request Execution

GraphQlService is the main Spring abstraction to call GraphQL Java to execute requests. Underlying transports, such as the Web Transports, delegate to GraphQlService to handle requests.

The main implementation, ExecutionGraphQlService, is a thin facade around the invocation of graphql.GraphQL. It is configured with a GraphQlSource for access to the graphql.GraphQL instance.

4.1. GraphQLSource

GraphQlSource is a core Spring abstraction for access to the graphql.GraphQL instance to use for request execution. It provides a builder API to initialize GraphQL Java and build a GraphQlSource.

The default GraphQlSource builder, accessible via GraphQlSource.builder(), enables support for Reactive DataFetcher, Context Propagation, and Exception Resolution.

4.1.1. Schema Resources

GraphQlSource.Builder can be configured with one or more Resource instances to be parsed and merged together. That means schema files can be loaded from just about any location. By default, the Spring Boot starter loads schema files from a well-known classpath location, but you can change that to a location on the file system via FileSystemResource, to byte content via ByteArrayResource, or implement a custom Resource that loads schema files from a remote location or storage.

4.1.2. Schema Creation

By default, GraphQlSource.Builder uses the GraphQL Java GraphQLSchemaGenerator to create the graphql.schema.GraphQLSchema. This works for most applications, but if necessary, you can hook into the schema creation through the builder:

GraphQlSource graphQlSource = GraphQlSource.builder()
        .schemaResources(..)
        .configureRuntimeWiring(..)
        .schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
            // create GraphQLSchema
        })
        .build();

The primary reason for this is to create the schema through a federation library.

4.1.3. RuntimeWiringConfigurer

You can use RuntimeWiringConfigurer to register:

  • Custom scalar types.

  • Directives handling code.

  • TypeResolver, if you need to override the Default TypeResolver for a type.

  • DataFetcher for a field, although most applications will simply configure AnnotatedControllerConfigurer, which detects annotated, DataFetcher handler methods. The Spring Boot starter adds the AnnotatedControllerConfigurer by default.

If you need to add a WiringFactory, e.g. to make registrations that take into account schema definitions, implement the alternative configure method that accepts both the RuntimeWiring.Builder and an output List<WiringFactory>. This allows you to add any number of factories that are then invoked in sequence.

The Spring for GraphQL Boot starter detects beans of type RuntimeWiringConfigurer.

4.1.4. Default TypeResolver

GraphQlSource.Builder registers ClassNameTypeResolver as the default TypeResolver to use for GraphQL Interfaces and Unions that don’t already have such a registration through a RuntimeWiringConfigurer. The purpose of a TypeResolver in GraphQL Java is to determine the GraphQL Object type for values returned from the DataFetcher for a GraphQL Interface or Union field.

ClassNameTypeResolver tries to match the simple class name of the value to a GraphQL Object Type and if it is not successful, it also navigates its super types including base classes and interfaces, looking for a match. ClassNameTypeResolver provides an option to configure a name extracting function along with Class to GraphQL Object type name mappings that should help to cover more corner cases.

4.2. Reactive DataFetcher

The default GraphQlSource builder enables support for a DataFetcher to return Mono or Flux which adapts those to a CompletableFuture where Flux values are aggregated and turned into a List, unless the request is a GraphQL subscription request, in which case the return value remains a Reactive Streams Publisher for streaming GraphQL responses.

A reactive DataFetcher can rely on access to Reactor context propagated from the transport layer, such as from a WebFlux request handling, see WebFlux Context.

4.3. Context Propagation

Spring for GraphQL provides support to transparently propagate context from the Web Transports, through the GraphQL engine, and to DataFetcher and other components it invokes. This includes both ThreadLocal context from the Spring MVC request handling thread and Reactor Context from the WebFlux processing pipeline.

4.3.1. WebMvc

A DataFetcher and other components invoked by GraphQL Java may not always execute on the same thread as the Spring MVC handler, for example if an asynchronous WebInterceptor or DataFetcher switches to a different thread.

Spring for GraphQL supports propagating ThreadLocal values from the Servlet container thread to the thread a DataFetcher and other components invoked by the GraphQL engine execute on. To do this, an application needs to create a ThreadLocalAccessor to extract ThreadLocal values of interest:

public class RequestAttributesAccessor implements ThreadLocalAccessor {

    private static final String KEY = RequestAttributesAccessor.class.getName();

    @Override
    public void extractValues(Map<String, Object> container) {
        container.put(KEY, RequestContextHolder.getRequestAttributes());
    }

    @Override
    public void restoreValues(Map<String, Object> values) {
        if (values.containsKey(KEY)) {
            RequestContextHolder.setRequestAttributes((RequestAttributes) values.get(KEY));
        }
    }

    @Override
    public void resetValues(Map<String, Object> values) {
        RequestContextHolder.resetRequestAttributes();
    }

}

A ThreadLocalAccessor can be registered in the WebGraphHandler builder. The Boot starter detects beans of this type and automatically registers them for Spring MVC application, see the Web Endpoints section.

4.3.2. WebFlux

A Reactive DataFetcher can rely on access to Reactor context that originates from the WebFlux request handling chain. This includes Reactor context added by WebInterceptor components.

4.4. Exception Resolution

GraphQL Java applications can register a DataFetcherExceptionHandler to decide how to represent exceptions from the data layer in the "errors" section of the GraphQL response.

Spring for GraphQL has a built-in DataFetcherExceptionHandler that is configured for use by the GraphQLSource builder. It enables applications to register one or more Spring DataFetcherExceptionResolver components that are invoked sequentially until one resolves the Exception to a list of graphql.GraphQLError objects.

DataFetcherExceptionResolver is an asynchronous contract. For most implementations, it would be sufficient to extend DataFetcherExceptionResolverAdapter and override one of its resolveToSingleError or resolveToMultipleErrors methods that resolve exceptions synchronously.

A GraphQLError can be assigned an graphql.ErrorClassification. Spring for GraphQL defines an ErrorType enum with common, error classification categories:

  • BAD_REQUEST

  • UNAUTHORIZED

  • FORBIDDEN

  • NOT_FOUND

  • INTERNAL_ERROR

Applications can use this to classify errors. If an error remains unresolved, by default it is marked as INTERNAL_ERROR.

4.5. Batch Loading

Given a Book and its Author, we can create one DataFetcher for a book and another for its author. This allows selecting books with or without authors, but it means books and authors aren’t loaded together, which is especially inefficient when querying multiple books as the author for each book is loaded individually. This is known as the N+1 select problem.

4.5.1. DataLoader

GraphQL Java provides a DataLoader mechanism for batch loading of related entities. You can find the full details in the GraphQL Java docs. Below is a summary of how it works:

  1. Register DataLoader's in the DataLoaderRegistry that can load entities, given unique keys.

  2. DataFetcher's can access DataLoader's and use them to load entities by id.

  3. A DataLoader defers loading by returning a future so it can be done in a batch.

  4. DataLoader's maintain a per request cache of loaded entities that can further improve efficiency.

4.5.2. BatchLoaderRegistry

The complete batching loading mechanism in GraphQL Java requires implementing one of several BatchLoader interface, then wrapping and registering those as DataLoaders with a name in the DataLoaderRegistry.

The API in Spring GraphQL is slightly different. For registration, there is only one, central BatchLoaderRegistry exposing factory methods and a builder to create and register any number of batch loading functions:

@Configuration
public class MyConfig {

    public MyConfig(BatchLoaderRegistry registry) {

        registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
            // return Mono<Map<Long, Author>
        });

        // more registrations ...
    }

}

The Spring Boot starter declares a BatchLoaderRegistry bean that you can inject into your configuration, as shown above, or into any component such as a controller in order register batch loading functions. In turn the BatchLoaderRegistry is injected into ExecutionGraphQlService where it ensures DataLoader registrations per request.

By default, the DataLoader name is based on the class name of the target entity. This allows an @SchemaMapping method to declare a DataLoader argument with a generic type, and without the need for specifying a name. The name, however, can be customized through the BatchLoaderRegistry builder, if necessary, along with other DataLoader options.

For many cases, when loading related entities, you can use @BatchMapping controller methods, which are a shortcut for and replace the need to use BatchLoaderRegistry and DataLoader directly. s BatchLoaderRegistry provides other important benefits too. It supports access to the same GraphQLContext from batch loading functions and from @BatchMapping methods, as well as ensures Context Propagation to them. This is why applications are expected to use it. It is possible to perform your own DataLoader registrations directly but such registrations would forgo the above benefits.

4.5.3. Testing Batch Loading

Start by having BatchLoaderRegistry perform registrations on a DataLoaderRegistry:

BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry();
// perform registrations...

DataLoaderRegistry dataLoaderRegistry = DataLoaderRegistry.newRegistry().build();
batchLoaderRegistry.registerDataLoaders(dataLoaderRegistry, graphQLContext);

Now you can access and test individual DataLoader's as follows:

DataLoader<Long, Book> loader = dataLoaderRegistry.getDataLoader(Book.class.getName());
loader.load(1L);
loader.loadMany(Arrays.asList(2L, 3L));
List<Book> books = loader.dispatchAndJoin(); // actual loading

assertThat(books).hasSize(3);
assertThat(books.get(0).getName()).isEqualTo("...");
// ...

5. Data Integration

5.1. Querydsl

Spring for GraphQL supports use of Querydsl to fetch data through the Spring Data Querydsl extension. Querydsl provides a flexible yet typesafe approach to express query predicates by generating a meta-model using annotation processors.

For example, declare a repository as QuerydslPredicateExecutor:

public interface AccountRepository extends Repository<Account, Long>,
            QuerydslPredicateExecutor<Account> {
}

Then use it to create a DataFetcher:

// For single result queries
DataFetcher<Account> dataFetcher =
        QuerydslDataFetcher.builder(repository).single();

// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
        QuerydslDataFetcher.builder(repository).many();

The DataFetcher builds a Querydsl Predicate from GraphQL request parameters, and uses it to fetch data. Spring Data supports QuerydslPredicateExecutor for JPA, MongoDB, and LDAP.

If the repository is ReactiveQuerydslPredicateExecutor, the builder returns DataFetcher<Mono<Account>> or DataFetcher<Flux<Account>>. Spring Data supports this variant for MongoDB.

5.1.1. Build Setup

To configure Querydsl in your build, follow the official reference documentation:

For example:

Gradle
dependencies {
    //...

    annotationProcessor "com.querydsl:querydsl-apt:$querydslVersion:jpa",
            'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final',
            'javax.annotation:javax.annotation-api:1.3.2'
}

compileJava {
    options.annotationProcessorPath = configurations.annotationProcessor
}
Maven
<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <version>${querydsl.version}</version>
        <classifier>jpa</classifier>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.hibernate.javax.persistence</groupId>
        <artifactId>hibernate-jpa-2.1-api</artifactId>
        <version>1.0.2.Final</version>
    </dependency>
    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>1.3.2</version>
    </dependency>
</dependencies>
<plugins>
    <!-- Annotation processor configuration -->
    <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>apt-maven-plugin</artifactId>
        <version>${apt-maven-plugin.version}</version>
        <executions>
            <execution>
                <goals>
                    <goal>process</goal>
                </goals>
                <configuration>
                    <outputDirectory>target/generated-sources/java</outputDirectory>
                    <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>

The webmvc-http sample uses Querydsl for artifactRepositories.

5.1.2. Customizations

QuerydslDataFetcher supports customizing how GraphQL arguments are bound onto properties to create a Querydsl Predicate. By default, arguments are bound as "is equal to" for each available property. To customize that, you can use QuerydslDataFetcher builder methods to provide a QuerydslBinderCustomizer.

A repository may itself be an instance of QuerydslBinderCustomizer. This is auto-detected and transparently applied during Auto Registration. However, when manually building a QuerydslDataFetcher you will need to use builder methods to apply it.

QuerydslDataFetcher supports interface and DTO projections to transform query results before returning these for further GraphQL processing.

5.1.3. Auto Registration

If a repository is annotated with @GraphQlRepository, it is automatically registered for queries that do not already have a registered DataFetcher and whose return type matches that of the repository domain type. This includes both single value and multi-value queries.

By default, the name of the GraphQL type returned by the query must match the simple name of the repository domain type. If needed, you can use the typeName attribute of @GraphQlRepository to specify the target GraphQL type name.

Auto-registration detects if a given repository implements QuerydslBinderCustomizer and transparently applies that through QuerydslDataFetcher builder methods.

Auto-registration is performed through a RuntimeWiringConfigurer which can be obtained from QuerydslDataFetcher. The Boot starter automatically detects @GraphQlRepository beans and uses them to initialize the RuntimeWiringConfigurer with.

5.2. Query by Example

Spring Data supports the use of Query by Example to fetch data. Query by Example (QBE) is a simple querying technique that does not require you to write queries through store-specific query languages.

Start by declaring a repository that is QueryByExampleExecutor:

public interface AccountRepository extends Repository<Account, Long>,
            QueryByExampleExecutor<Account> {
}

Use QueryByExampleDataFetcher to turn the repository into a DataFecher:

// For single result queries
DataFetcher<Account> dataFetcher =
        QueryByExampleDataFetcher.builder(repository).single();

// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
        QueryByExampleDataFetcher.builder(repository).many();

The DataFetcher uses the GraphQL arguments map to create the domain type of the repository and use that as the example object to fetch data with. Spring Data supports QueryByExampleDataFetcher for JPA, MongoDB, Neo4j, and Redis.

If the repository is ReactiveQueryByExampleExecutor, the builder returns DataFetcher<Mono<Account>> or DataFetcher<Flux<Account>>. Spring Data supports this variant for MongoDB, Neo4j, Redis, and R2dbc.

5.2.1. Build Setup

Query by Example is already included in the Spring Data modules for the data stores where it is supported, so no extra setup is required to enable it.

5.2.2. Customizations

QueryByExampleDataFetcher supports interface and DTO projections to transform query results before returning these for further GraphQL processing.

5.2.3. Auto Registration

If a repository is annotated with @GraphQlRepository, it is automatically registered for queries that do not already have a registered DataFetcher and whose return type matches that of the repository domain type. This includes both single value and multi-value queries.

By default, the name of the GraphQL type returned by the query must match the simple name of the repository domain type. If needed, you can use the typeName attribute of @GraphQlRepository to specify the target GraphQL type name.

Auto-registration is performed through a RuntimeWiringConfigurer which can be obtained from QueryByExampleDataFetcher. The Boot starter automatically detects @GraphQlRepository beans and uses them to initialize the RuntimeWiringConfigurer with.

6. 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".

6.1. 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 Spring 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.

6.2. @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..

6.2.1. Method Signature

Schema mapping handler methods can have any of the following method arguments:

Method Argument Description

@Argument

For access to field arguments with conversion. See @Argument.

@ProjectedPayload Interface

For access to field arguments through a project interface. See @ProjectPayload Interface.

Source

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

DataLoader

For access to a DataLoader in the DataLoaderRegistry. See DataLoader.

@ContextValue

For access to a value from the localContext, if it is an instance of GraphQLContext, or from the GraphQLContext of DataFetchingEnvironment.

GraphQLContext

For access to the context from the DataFetchingEnvironment.

java.security.Principal

Obtained from Spring Security context, if available.

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.

Schema mapping handler methods can return any value, including Reactor Mono and Flux as described in Reactive DataFetcher.

6.2.2. @Argument

In GraphQL Java, the DataFetchingEnvironment provides access to field-specific argument values. The arguments are available as simple scalar values such as String, or as a Map of values for more complex input, or a List of values.

Use @Argument to access an argument for the field that maps to the handler method. You can declare such a method parameter to be of any type.

@Controller
public class BookController {

    @QueryMapping
    public Book bookById(@Argument Long id) {
        // ...
    }

    @MutationMapping
    public Book addBook(@Argument BookInput bookInput) {
        // ...
    }
}

You can explicitly specify the argument name, for example @Argument("bookInput"), or if it not specified, it defaults to the method parameter name, but this requires the -parameters compiler flag with Java 8+ or debugging information from the compiler.

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 the GraphQL Engine.

You can use @Argument on a Map<String, Object> argument, to obtain all argument values. The name attribute on @Argument must not be set.

6.2.3. @Argument validation

If a Bean Validation Validator (or typically, a LocalValidatorFactoryBean) bean is present in the application context, the AnnotatedControllerConfigurer will auto-detect it and configure support for validation. Controller arguments annotated with @Valid and @Validated are then validated before method invocation.

Bean Validation lets you declare constraints on types, as the following example shows:

public class BookInput {

    @NotNull
    private String title;

    @NotNull
    @Size(max=13)
    private String isbn;
}

We can then mark our argument for validation with @Valid:

@Controller
public class BookController {

    @MutationMapping
    public Book addBook(@Argument @Valid BookInput bookInput) {
        // ...
    }
}

If an error occurs during validation, a ConstraintViolationException is thrown and can be later resolved with a custom DataFetcherExceptionResolver.

Unlike Spring MVC, handler method signatures do not support the injection of BindingResult for reacting to validation errors: those are globally dealt with as exceptions.

6.2.4. @ProjectPayload 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();
}

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

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

6.3. @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.

6.3.1. Method Signature

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.

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>, List<V>

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

7. Security

The path to a Web GraphQL endpoint can be secured with HTTP URL security to ensure that only authenticated users can access it. This does not, however, differentiate among different GraphQL requests on such a shared endpoint on a single URL.

To apply more fine-grained security, add Spring Security annotations such as @PreAuthorize or @Secured to service methods involved in fetching specific parts of the GraphQL response. This should work due to Context Propagation that aims to make Security, and other context, available at the data fetching level.

The Spring for GraphQL repository contains samples for Spring MVC and for WebFlux.

8. Testing

It’s possible to test GraphQL requests with Spring’s WebTestClient, just sending and receiving JSON, but a number of GraphQL specific details make this approach more cumbersome than is necessary.

To get the full testing support, you’ll need to add the spring-graphql-test dependdency in your build:

Gradle
dependencies {
    // ...
    testImplementation 'org.springframework.graphql:spring-graphql-test:1.0.0-M5'
}
Maven
<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.springframework.graphql</groupId>
        <artifactId>spring-graphql-test</artifactId>
        <version>1.0.0-M5</version>
        <scope>test</scope>
    </dependency>
</dependencies>

8.1. GraphQlTester

GraphQlTester defines a workflow to test GraphQL requests with the following benefits:

  • Verify no unexpected errors under the "errors" key in the response.

  • Decode under the "data" key in the response.

  • Use JsonPath to decode different parts of the response.

  • Test subscriptions.

To create GraphQlTester, you only need a GraphQlService, and no transport:

GraphQlSource graphQlSource = GraphQlSource.builder()
        .schemaResources(...)
        .runtimeWiringConfigurer(...)
        .build();

GraphQlService graphQlService = new ExecutionGraphQlService(graphQlSource);

GraphQlTester graphQlTester = GraphQlTester.builder(graphQlService).build();

8.2. WebGraphQlTester

WebGraphQlTester extends GraphQlTester to add a workflow and configuration specific to Web Transports, and it always verifies GraphQL HTTP responses are 200 (OK).

To create WebGraphQlTester, you need one of the following inputs:

  • WebTestClient — perform requests as an HTTP client, either against HTTP handlers without a server, or against a live server.

  • WebGraphQlHandler — perform requests through the Web Interception chain used by both HTTP and WebSocket handlers, which in effect is testing without a Web framework. One reason to use this is for Subscriptions.

For Spring WebFlux without a server, you can point to your Spring configuration:

ApplicationContext context = ... ;

WebTestClient client =
        WebTestClient.bindToApplicationContext(context)
                .configureClient()
                .baseUrl("/graphql")
                .build();

WebGraphQlTester tester = WebGraphQlTester.builder(client).build();

For Spring MVC without a server, the same but using MockMvcWebTestClient:

WebApplicationContext context = ... ;

WebTestClient client =
        MockMvcWebTestClient.bindToApplicationContext(context)
                .configureClient()
                .baseUrl("/graphql")
                .build();

WebGraphQlTester tester = WebGraphQlTester.builder(client).build();

To test against a live, running server:

WebTestClient client =
        WebTestClient.bindToServer()
                .baseUrl("http://localhost:8080/graphql")
                .build();

WebGraphQlTester tester = WebGraphQlTester.builder(client).build();

WebGraphQlTester supports setting HTTP request headers and access to HTTP response headers. This may be useful to inspect or set security related headers.

this.graphQlTester.queryName("{ myQuery }")
        .httpHeaders(headers -> headers.setBasicAuth("rob", "..."))
        .execute()
        .httpHeadersSatisfy(headers -> {
            // check response headers
        })
        .path("myQuery.field1").entity(String.class).isEqualTo("value1")
        .path("myQuery.field2").entity(String.class).isEqualTo("value2");

You can also set default request headers at the builder level:

WebGraphQlTester tester = WebGraphQlTester.builder(client)
    .defaultHttpHeaders(headers -> headers.setBasicAuth("rob", "..."))
    .build();

8.3. Queries

Below is an example query test using JsonPath to extract all release versions in the GraphQL response.

String query = "{" +
        "  project(slug:\"spring-framework\") {" +
        "   releases {" +
        "     version" +
        "   }"+
        "  }" +
        "}";

graphQlTester.query(query)
        .execute()
        .path("project.releases[*].version")
        .entityList(String.class)
        .hasSizeGreaterThan(1);

The JsonPath is relative to the "data" section of the response.

You can also create query files with extensions .graphql or .gql under "graphql/" on the classpath and refer to them by file name. For example, given a file called projectReleases.graphql in src/main/resources/graphql, with content:

query projectReleases($slug: ID!) {
    project(slug: $slug) {
        releases {
            version
        }
    }
}

You can write the same test as follows:

graphQlTester.queryName("projectReleases") (1)
        .variable("slug", "spring-framework") (2)
        .execute()
        .path("project.releases[*].version")
        .entityList(String.class)
        .hasSizeGreaterThan(1);
1 Refer to the query in the file named "projectReleases".
2 Set the slug variable.

The "JS GraphQL" plugin for IntelliJ supports GraphQL query files with code completion.

8.4. Errors

Verify won’t succeed when there are errors under the "errors" key in the response.

If necessary to ignore an error, use an error filter Predicate:

graphQlTester.query(query)
        .execute()
        .errors()
        .filter(error -> ...)
        .verify()
        .path("project.releases[*].version")
        .entityList(String.class)
        .hasSizeGreaterThan(1);

An error filter can be registered globally and apply to all tests:

WebGraphQlTester graphQlTester = WebGraphQlTester.builder(client)
        .errorFilter(error -> ...)
        .build();

Or to expect an error, and in contrast to filter, throw an assertion error when it doesn’t exist in the response:

graphQlTester.query(query)
        .execute()
        .errors()
        .expect(error -> ...)
        .verify()
        .path("project.releases[*].version")
        .entityList(String.class)
        .hasSizeGreaterThan(1);

Or inspect all errors directly and that also marks them as filtered:

graphQlTester.query(query)
        .execute()
        .errors()
        .satisfy(errors -> {
            // ...
        });

If a request does not have any response data (e.g. mutation), use executeAndVerify instead of execute to verify there are no errors in the response:

graphQlTester.query(query).executeAndVerify();

8.5. Subscriptions

The executeSubscription method defines a workflow specific to subscriptions which return a stream of responses instead of a single response.

To test subscriptions, you can create GraphQlTester with a GraphQlService, which calls graphql.GraphQL directly and that returns a stream of responses:

GraphQlService service = ... ;

GraphQlTester graphQlTester = GraphQlTester.builder(service).build();

Flux<String> result = graphQlTester.query("subscription { greetings }")
    .executeSubscription()
    .toFlux("greetings", String.class);  // decode each response

The StepVerifier from Project Reactor is useful to verify a stream:

Flux<String> result = graphQlTester.query("subscription { greetings }")
    .executeSubscription()
    .toFlux("greetings", String.class);

StepVerifier.create(result)
        .expectNext("Hi")
        .expectNext("Bonjour")
        .expectNext("Hola")
        .verifyComplete();

To test with the Web Interception chain, you can create WebGraphQlTester with a WebGraphQlHandler:

GraphQlService service = ... ;

WebGraphQlHandler handler = WebGraphQlHandler.builder(service)
    .interceptor((input, next) -> next.handle(input))
    .build();

WebGraphQlTester graphQlTester = WebGraphQlTester.builder(handler).build();

Currently, Spring for GraphQL does not support testing with a WebSocket client, and it cannot be used for integration test of GraphQL over WebSocket requests.

9. Samples

This Spring for GraphQL repository contains sample applications for various scenarios.

You can run those by cloning this repository and running main application classes from your IDE or by typing the following on the command line:

$ ./gradlew :samples:{sample-directory-name}:bootRun