1. Overview

Spring for GraphQL provides support for Spring applications built on GraphQL Java. It is a joint collaboration between the GraphQL Java team and Spring engineering.

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.

Please, use our issue tracker to report a problem, discuss a design issue, or to request a feature.

Check the Wiki. for what’s new, baseline requirements, and upgrade notes, and other cross-version information.

To get started, see the Boot Starter and Samples sections.

2. Server Transports

Spring for GraphQL supports server handling of GraphQL requests over HTTP, WebSocket, and RSocket.

2.1. HTTP

GraphQlHttpHandler handles GraphQL over HTTP requests and delegates to the 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 "application/json" as content type and 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. The default and preferred choice of media type is "application/graphql-response+json", but "application/json" is also supported, as described in the specification.

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 1.0.x branch of this repository contains a Spring MVC HTTP sample application.

2.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 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. By default, the Boot Starter does not expose a GraphQL over WebSocket endpoint, but it’s easy to enable it by adding a property for the endpoint path. Please, see the Web Endpoints section for details, or check the GraphQlWebMvcAutoConfiguration or the GraphQlWebFluxAutoConfiguration for the actual Boot starter config.

The 1.0.x branch of this repository contains a WebFlux WebSocket sample application.

2.3. RSocket

GraphQlRSocketHandler handles GraphQL over RSocket requests. Queries and mutations are expected and handled as an RSocket request-response interaction while subscriptions are handled as request-stream.

GraphQlRSocketHandler can be used a delegate from an @Controller that is mapped to the route for GraphQL requests. For example:

import java.util.Map;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.springframework.graphql.server.GraphQlRSocketHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;

@Controller
public class GraphQlRSocketController {

    private final GraphQlRSocketHandler handler;

    GraphQlRSocketController(GraphQlRSocketHandler handler) {
        this.handler = handler;
    }

    @MessageMapping("graphql")
    public Mono<Map<String, Object>> handle(Map<String, Object> payload) {
        return this.handler.handle(payload);
    }

    @MessageMapping("graphql")
    public Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) {
        return this.handler.handleSubscription(payload);
    }
}

2.4. Interception

Server transports allow intercepting requests before and after the GraphQL Java engine is called to process a request.

2.4.1. WebGraphQlInterceptor

HTTP and WebSocket transports invoke a chain of 0 or more WebGraphQlInterceptor, followed by an ExecutionGraphQlService that calls the GraphQL Java engine. WebGraphQlInterceptor allows an application to intercept incoming requests and do one of the following:

  • Check HTTP request details

  • Customize the graphql.ExecutionInput

  • Add HTTP response headers

  • Customize the graphql.ExecutionResult

For example, an interceptor can pass an HTTP request header to a DataFetcher:

import java.util.Collections;

import reactor.core.publisher.Mono;

import org.springframework.graphql.data.method.annotation.ContextValue;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.stereotype.Controller;

class RequestHeaderInterceptor implements WebGraphQlInterceptor { (1)

    @Override
    public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
        String value = request.getHeaders().getFirst("myHeader");
        request.configureExecutionInput((executionInput, builder) ->
                builder.graphQLContext(Collections.singletonMap("myHeader", value)).build());
        return chain.next(request);
    }
}

@Controller
class MyContextValueController { (2)

    @QueryMapping
    Person person(@ContextValue String myHeader) {
        ...
    }
}
1 Interceptor adds HTTP request header value into GraphQLContext
2 Data controller method accesses the value

Reversely, an interceptor can access values added to the GraphQLContext by a controller:

import graphql.GraphQLContext;
import reactor.core.publisher.Mono;

import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Controller;

// Subsequent access from a WebGraphQlInterceptor

class ResponseHeaderInterceptor implements WebGraphQlInterceptor {

    @Override
    public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) { (2)
        return chain.next(request).doOnNext(response -> {
            String value = response.getExecutionInput().getGraphQLContext().get("cookieName");
            ResponseCookie cookie = ResponseCookie.from("cookieName", value).build();
            response.getResponseHeaders().add(HttpHeaders.SET_COOKIE, cookie.toString());
        });
    }
}

@Controller
class MyCookieController {

    @QueryMapping
    Person person(GraphQLContext context) { (1)
        context.put("cookieName", "123");
        ...
    }
}
1 Controller adds value to the GraphQLContext
2 Interceptor uses the value to add an HTTP response header

WebGraphQlHandler can modify the ExecutionResult, for example, to inspect and modify request validation errors that are raised before execution begins and which cannot be handled with a DataFetcherExceptionResolver:

import java.util.List;

import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import reactor.core.publisher.Mono;

import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;

class RequestErrorInterceptor implements WebGraphQlInterceptor {

    @Override
    public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
        return chain.next(request).map(response -> {
            if (response.isValid()) {
                return response; (1)
            }

            List<GraphQLError> errors = response.getErrors().stream() (2)
                    .map(error -> {
                        GraphqlErrorBuilder<?> builder = GraphqlErrorBuilder.newError();
                        // ...
                        return builder.build();
                    })
                    .toList();

            return response.transform(builder -> builder.errors(errors).build()); (3)
        });
    }
}
1 Return the same if ExecutionResult has a "data" key with non-null value
2 Check and transform the GraphQL errors
3 Update the ExecutionResult with the modified errors

Use WebGraphQlHandler to configure the WebGraphQlInterceptor chain. This is supported by the Boot Starter, see Web Endpoints.

2.4.2. RSocketQlInterceptor

Similar to WebGraphQlInterceptor, an RSocketQlInterceptor allows intercepting GraphQL over RSocket requests before and after GraphQL Java engine execution. You can use this to customize the graphql.ExecutionInput and the graphql.ExecutionResult.

3. Request Execution

ExecutionGraphQlService is the main Spring abstraction to call GraphQL Java to execute requests. Underlying transports, such as the HTTP, delegate to ExecutionGraphQlService to handle requests.

The main implementation, DefaultExecutionGraphQlService, is configured with a GraphQlSource for access to the graphql.GraphQL instance to invoke.

3.1. GraphQLSource

GraphQlSource is a contract to expose the graphql.GraphQL instance to use that also includes a builder API to build that instance. The default builder is available via GraphQlSource.schemaResourceBuilder().

The Boot Starter creates an instance of this builder and further initializes it to load schema files from a configurable location, to expose properties to apply to GraphQlSource.Builder, to detect RuntimeWiringConfigurer beans, Instrumentation beans for GraphQL metrics, and DataFetcherExceptionResolver and SubscriptionExceptionResolver beans for exception resolution. For further customizations, you can also declare a GraphQlSourceBuilderCustomizer bean, for example:

@Configuration(proxyBeanMethods = false)
class GraphQlConfig {

    @Bean
    public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
        return (builder) ->
                builder.configureGraphQl(graphQlBuilder ->
                        graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
    }
}

3.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 Boot starter looks for schema files with extensions ".graphqls" or ".gqls" under the location classpath:graphql/**, which is typically src/main/resources/graphql. You can also use a file system location, or any location supported by the Spring Resource hierarchy, including a custom implementation that loads schema files from remote locations, from storage, or from memory.

Use classpath*:graphql/**/ to find schema files across multiple classpath locations, e.g. across multiple modules.

3.1.2. Schema Creation

By default, GraphQlSource.Builder uses the GraphQL Java SchemaGenerator to create the graphql.schema.GraphQLSchema. This works for typical use, but if you need to use a different generator, e.g. for federation, you can register a schemaFactory callback:

GraphQlSource.Builder builder = ...

builder.schemaResources(..)
        .configureRuntimeWiring(..)
        .schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
            // create GraphQLSchema
        })

The GraphQlSource section explains how to configure that with Spring Boot.

3.1.3. Schema Traversal

You can register a graphql.schema.GraphQLTypeVisitor via builder.schemaResources(..).typeVisitors(..) if you want to traverse the schema after it is created, and possibly apply changes to the GraphQLCodeRegistry. Keep in mind, however, that such a visitor cannot change the schema. See Schema Transformation, if you need to make changes to the schema.

3.1.4. Schema Transformation

You can register a graphql.schema.GraphQLTypeVisitor via builder.schemaResources(..).typeVisitorsToTransformSchema(..) if you want to traverse and transform the schema after it is created, and make changes to the schema. Keep in mind that this is more expensive than Schema Traversal so generally prefer traversal to transformation unless you need to make schema changes.

3.1.5. 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 Boot Starter adds the AnnotatedControllerConfigurer by default.

GraphQL Java, server applications use Jackson only for serialization to and from maps of data. Client input is parsed into a map. Server output is assembled into a map based on the field selection set. This means you can’t rely on Jackson serialization/deserialization annotations. Instead, you can use custom scalar types.

The Boot Starter detects beans of type RuntimeWiringConfigurer and registers them in the GraphQlSource.Builder. That means in most cases, you’ll' have something like the following in your configuration:

@Configuration
public class GraphQlConfig {

    @Bean
    public RuntimeWiringConfigurer runtimeWiringConfigurer(BookRepository repository) {

        GraphQLScalarType scalarType = ... ;
        SchemaDirectiveWiring directiveWiring = ... ;
        DataFetcher dataFetcher = QuerydslDataFetcher.builder(repository).single();

        return wiringBuilder -> wiringBuilder
                .scalar(scalarType)
                .directiveWiring(directiveWiring)
                .type("Query", builder -> builder.dataFetcher("book", dataFetcher));
    }
}

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.

3.1.6. 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:

GraphQlSource.Builder builder = ...
ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver();
classNameTypeResolver.setClassNameExtractor((klass) -> {
    // Implement Custom ClassName Extractor here
});
builder.defaultTypeResolver(classNameTypeResolver);

The GraphQlSource section explains how to configure that with Spring Boot.

3.1.7. Operation Caching

GraphQL Java must parse and validate an operation before executing it. This may impact performance significantly. To avoid the need to re-parse and validate, an application may configure a PreparsedDocumentProvider that caches and reuses Document instances. The GraphQL Java docs provide more details on query caching through a PreparsedDocumentProvider.

In Spring GraphQL you can register a PreparsedDocumentProvider through GraphQlSource.Builder#configureGraphQl: .

// Typically, accessed through Spring Boot's GraphQlSourceBuilderCustomizer
GraphQlSource.Builder builder = ...

// Create provider
PreparsedDocumentProvider provider = ...

builder.schemaResources(..)
        .configureRuntimeWiring(..)
        .configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider))

The GraphQlSource section explains how to configure that with Spring Boot.

3.1.8. Directives

The GraphQL language supports directives that "describe alternate runtime execution and type validation behavior in a GraphQL document". Directives are similar to annotations in Java but declared on types, fields, fragments and operations in a GraphQL document.

GraphQL Java provides the SchemaDirectiveWiring contract to help applications detect and handle directives. For more details, see Schema Directives in the GraphQL Java documentation.

In Spring GraphQL you can register a SchemaDirectiveWiring through a RuntimeWiringConfigurer. The Boot Starter detects such beans, so you might have something like:

@Configuration
public class GraphQlConfig {

     @Bean
     public RuntimeWiringConfigurer runtimeWiringConfigurer() {
          return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
     }

}
For an example of directives support check out the Extended Validation for Graphql Java library.

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

3.3. Context Propagation

Spring for GraphQL provides support to transparently propagate context from the HTTP, through GraphQL Java, 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.

3.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 WebGraphQlInterceptor 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 GraphQL Java to execute on. To do this, an application needs to implement io.micrometer.context.ThreadLocalAccessor for a ThreadLocal values of interest:

public class RequestAttributesAccessor implements ThreadLocalAccessor<RequestAttributes> {

    @Override
    public Object key() {
        return RequestAttributesAccessor.class.getName();
    }

    @Override
    public RequestAttributes getValue() {
        return RequestContextHolder.getRequestAttributes();
    }

    @Override
    public void setValue(RequestAttributes attributes) {
        RequestContextHolder.setRequestAttributes(attributes);
    }

    @Override
    public void reset() {
        RequestContextHolder.resetRequestAttributes();
    }

}

You can register a ThreadLocalAccessor manually on startup with the global ContextRegistry instance, which is accessible via io.micrometer.context.ContextRegistry#getInstance(). You can also register it automatically through the java.util.ServiceLoader mechanism.

3.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 WebGraphQlInterceptor components.

3.4. Exception Resolution

A GraphQL Java application 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 default GraphQLSource builder. It allows applications to register one or more Spring DataFetcherExceptionResolver components that are invoked sequentially until one resolves the Exception to a (possibly empty) 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 to a category via graphql.ErrorClassification. In Spring GraphQL, you can also assign via ErrorType which has the following common classifications that applications can use to categorize errors:

  • BAD_REQUEST

  • UNAUTHORIZED

  • FORBIDDEN

  • NOT_FOUND

  • INTERNAL_ERROR

If an exception remains unresolved, by default it is categorized as an INTERNAL_ERROR with a generic message that includes the category name and the executionId from DataFetchingEnvironment. The message is intentionally opaque to avoid leaking implementation details. Applications can use a DataFetcherExceptionResolver to customize error details.

Unresolved exception are logged at ERROR level along with the executionId to correlate to the error sent to the client. Resolved exceptions are logged at DEBUG level.

3.4.1. Request Exceptions

The GraphQL Java engine may run into validation or other errors when parsing the request and that in turn prevent request execution. In such cases, the response contains a "data" key with null and one or more request-level "errors" that are global, i.e. not having a field path.

DataFetcherExceptionResolver cannot handle such global errors because they are raised before execution begins and before any DataFetcher is invoked. An application can use transport level interceptors to inspect and transform errors in the ExecutionResult. See examples under WebGraphQlInterceptor.

3.4.2. Subscription Exceptions

The Publisher for a subscription request may complete with an error signal in which case the underlying transport (e.g. WebSocket) sends a final "error" type message with a list of GraphQL errors.

DataFetcherExceptionResolver cannot resolve errors from a subscription Publisher, since the data DataFetcher only creates the Publisher initially. After that, the transport subscribes to the Publisher that may then complete with an error.

An application can register a SubscriptionExceptionResolver in order to resolve exceptions from a subscription Publisher in order to resolve those to GraphQL errors to send to the client.

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

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

3.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 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 DefaultExecutionGraphQlService 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 DataLoaderOptions.

To configure default DataLoaderOptions globally, to use as a starting point for any registration, you can override Boot’s BatchLoaderRegistry bean and use the constructor for DefaultBatchLoaderRegistry that accepts Supplier<DataLoaderOptions>.

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.

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.

3.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("...");
// ...

4. Data Integration

Spring for GraphQL lets you leverage existing Spring technology, following common programming models to expose underlying data sources through GraphQL.

This section discusses an integration layer for Spring Data that provides an easy way to adapt a Querydsl or a Query by Example repository to a DataFetcher, including the option for automated detection and GraphQL Query registration for repositories marked with @GraphQlRepository.

4.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();

You can now register the above DataFetcher through a RuntimeWiringConfigurer.

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.

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

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

To learn what projections are, please refer to the Spring Data docs. To understand how to use projections in GraphQL, please see Selection Set vs Projections.

To use Spring Data projections with Querydsl repositories, create either a projection interface or a target DTO class and configure it through the projectAs method to obtain a DataFetcher producing the target type:

class Account {

    String name, identifier, description;

    Person owner;
}

interface AccountProjection {

    String getName();

    String getIdentifier();
}

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

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

4.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 built-in RuntimeWiringConfigurer that can be obtained from QuerydslDataFetcher. The Boot Starter automatically detects @GraphQlRepository beans and uses them to initialize the RuntimeWiringConfigurer with.

Auto-registration applies customizations by calling customize(Builder) on the repository instance if your repository implements QuerydslBuilderCustomizer or ReactiveQuerydslBuilderCustomizer respectively.

4.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 DataFetcher:

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

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

You can now register the above DataFetcher through a RuntimeWiringConfigurer.

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.

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

4.2.2. Customizations

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

To learn what projections are, please refer to the Spring Data documentation. To understand the role of projections in GraphQL, please see Selection Set vs Projections.

To use Spring Data projections with Query by Example repositories, create either a projection interface or a target DTO class and configure it through the projectAs method to obtain a DataFetcher producing the target type:

class Account {

    String name, identifier, description;

    Person owner;
}

interface AccountProjection {

    String getName();

    String getIdentifier();
}

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

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

4.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 built-in RuntimeWiringConfigurer that can be obtained from QueryByExampleDataFetcher. The Boot Starter automatically detects @GraphQlRepository beans and uses them to initialize the RuntimeWiringConfigurer with.

Auto-registration applies customizations by calling customize(Builder) on the repository instance if your repository implements QueryByExampleBuilderCustomizer or ReactiveQueryByExampleBuilderCustomizer respectively.

4.3. Selection Set vs Projections

A common question that arises is, how GraphQL selection sets compare to Spring Data projections and what role does each play?

The short answer is that Spring for GraphQL is not a data gateway that translates GraphQL queries directly into SQL or JSON queries. Instead, it lets you leverage existing Spring technology and does not assume a one for one mapping between the GraphQL schema and the underlying data model. That is why client-driven selection and server-side transformation of the data model can play complementary roles.

To better understand, consider that Spring Data promotes domain-driven (DDD) design as the recommended approach to manage complexity in the data layer. In DDD, it is important to adhere to the constraints of an aggregate. By definition an aggregate is valid only if loaded in its entirety, since a partially loaded aggregate may impose limitations on aggregate functionality.

In Spring Data you can choose whether you want your aggregate be exposed as is, or whether to apply transformations to the data model before returning it as a GraphQL result. Sometimes it’s enough to do the former, and by default the Querydsl and the Query by Example integrations turn the GraphQL selection set into property path hints that the underlying Spring Data module uses to limit the selection.

In other cases, it’s useful to reduce or even transform the underlying data model in order to adapt to the GraphQL schema. Spring Data supports this through Interface and DTO Projections.

Interface projections define a fixed set of properties to expose where properties may or may not be null, depending on the data store query result. There are two kinds of interface projections both of which determine what properties to load from the underlying data source:

  • Closed interface projections are helpful if you cannot partially materialize the aggregate object, but you still want to expose a subset of properties.

  • Open interface projections leverage Spring’s @Value annotation and SpEL expressions to apply lightweight data transformations, such as concatenations, computations, or applying static functions to a property.

DTO projections offer a higher level of customization as you can place transformation code either in the constructor or in getter methods.

DTO projections materialize from a query where the individual properties are determined by the projection itself. DTO projections are commonly used with full-args constructors (e.g. Java records), and therefore they can only be constructed if all required fields (or columns) are part of the database query result.

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

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

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

5.2.1. Method Signature

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 map of arguments, where @Argument does not have a name attribute.

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.

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.

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.

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

5.2.2. @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) {
        // ...
    }
}

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 map of all argument values. The name attribute on @Argument must not be set.

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

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

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

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

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

5.2.8. 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 Exception Resolution 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.

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

5.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>, 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.

6. 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 1.0.x branch of this repository contains samples for Spring MVC and for WebFlux.

7. Observability

Observability support with Micrometer is directly instrumented in Spring for GraphQL. This enables both metrics and traces for GraphQL requests and "non-trivial" data fetching operations. Because the GraphQL engine operates on top of a transport layer, you should also expect observations from the transport, if supported in Spring Framework.

Observations are only published if an ObservationRegistry is configured in the application. You can learn more about configuring the observability infrastructure in Spring Boot. If you would like to customize the metadata produced with the GraphQL observations, you can configure a custom convention on the instrumentation directly. If your application is using Spring Boot, contributing the custom convention as a bean is the preferred way.

7.1. Server Requests instrumentation

GraphQL Server Requests observations are created with the name "graphql.request" for Servlet and Reactive applications and above all supported transports. Applications need to configure the org.springframework.graphql.observation.GraphQlObservationInstrumentation instrumentation in their application. It is using the org.springframework.graphql.observation.DefaultExecutionRequestObservationConvention by default, backed by the ExecutionRequestObservationContext.

By default, the following KeyValues are created:

Table 1. Low cardinality Keys

Name

Description

graphql.operation (required)

GraphQL Operation name.

graphql.outcome (required)

Outcome of the GraphQL request.

The graphql.operation KeyValue will use the custom name of the provided query, or the standard name for the operation if none ("query", "mutation" or "subscription"). The graphql.outcome KeyValue will be "SUCCESS" if a valid GraphQL response has been sent, "REQUEST_ERROR" if the request could not be parsed, or "INTERNAL_ERROR" if no valid GraphQL response could be produced.

Table 2. High cardinality Keys

Name

Description

graphql.execution.id (required)

graphql.execution.ExecutionId of the GraphQL request.

7.2. DataFetcher instrumentation

GraphQL DataFetcher observations are created with the name "graphql.datafetcher", only for data fetching operations that are considered as "non trivial" (property fetching on a Java object is a trivial operation). Applications need to configure the org.springframework.graphql.observation.GraphQlObservationInstrumentation instrumentation in their application. It is using the org.springframework.graphql.observation.DefaultDataFetcherObservationConvention by default, backed by the DataFetcherObservationContext.

By default, the following KeyValues are created:

Table 3. Low cardinality Keys

Name

Description

graphql.error.type (required)

Class name of the data fetching error

graphql.field.name (required)

Name of the field being fetched.

graphql.outcome (required)

Outcome of the GraphQL data fetching operation, "SUCCESS" or "ERROR".

Table 4. High cardinality Keys

Name

Description

graphql.field.path (required)

Path to the field being fetched (for example, "/bookById").

8. GraalVM Native support

Spring Framework 6.0 introduced the support infrastructure for compiling Spring applications to GraalVM Native images. If you are not familiar with GraalVM in general, how this differs from applications deployed on the JVM and what it means for Spring application, please refer to the dedicated Spring Boot 3.0 GraalVM Native Image support documentation. Spring Boot also documents the know limitations with the GraalVM support in Spring.

8.1. GraphQL Java metadata

Since the static analysis of your application is done at build time, GraalVM might need extra hints if your application is looking up static resources, performing reflection or creating JDK proxies at runtime.

GraphQL Java is performing three tasks at runtime that Native Images are sensible to:

  1. Loading resource bundles for message internationalization

  2. Some reflection on internal types for schema inspection

  3. Reflection on Java types that your application registers with the schema. This happens for example when GraphQL Java is fetching properties from application types

The first two items are handled via reachability metadata that has been contributed by the Spring team to the GraalVM reachability metadata repository. This metadata is automatically fetched by the native compilation tool when building an application that depends on GraphQL Java. This doesn’t cover our third item in the list, as those types are provided by the application itself and must be discovered by another mean.

8.2. Native Server applications support

In typical Spring for GraphQL applications, Java types tied to the GraphQL schema are exposed in @Controller method signatures as parameters or return types. During the Ahead Of Time processing phase of the build, Spring or GraphQL will use its o.s.g.data.method.annotation.support.SchemaMappingBeanFactoryInitializationAotProcessor to discover the relevant types and register reachability metadata accordingly. This is all done automatically for you if you are building a Spring Boot application with GraalVM support.

If your application is "manually" registering data fetchers, some types are not discoverable as a result. You should then register them with Spring Framework’s @RegisterReflectionForBinding:

import graphql.schema.DataFetcher;

import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.data.query.QuerydslDataFetcher;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;

@Configuration
@RegisterReflectionForBinding(Book.class) (3)
public class GraphQlConfiguration {

    @Bean
    RuntimeWiringConfigurer customWiringConfigurer(BookRepository bookRepository) { (1)
        DataFetcher<Book> dataFetcher = QuerydslDataFetcher.builder(bookRepository).single();
        return wiringBuilder -> wiringBuilder
                .type("Query", builder -> builder.dataFetcher("book", dataFetcher)); (2)
    }

}
1 This application declares a RuntimeWiringConfigurer that "manually" adds a DataFetcher
2 Through this DataFetcher, the BookRepository will expose a Book type
3 @RegisterReflectionForBinding will register the relevant hints for the Book type and all types exposed as fields

8.3. Client support

The GraphQlClient is not necessarily present as a bean in the application context and it does not expose the Java types used in the schema in method signatures. The AotProcessor strategy described in the section above cannot be used as a result. For client support, Spring for GraphQL embeds the relevant reachability metadata for the client infrastructure. When it comes to Java types used by the application, applications should use a similar strategy as "manual" data fetchers using @RegisterReflectionForBinding:

import reactor.core.publisher.Mono;

import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.graphql.client.GraphQlClient;
import org.springframework.stereotype.Component;

@Component
@RegisterReflectionForBinding(Project.class) (2)
public class ProjectService {

    private final GraphQlClient graphQlClient;

    public ProjectService(GraphQlClient graphQlClient) {
        this.graphQlClient = graphQlClient;
    }

    public Mono<Project> project(String projectSlug) {
        String document = """
                query projectWithReleases($projectSlug: ID!) {
                    project(slug: $projectSlug) {
                        name
                        releases {
                            version
                        }
                    }
                }
                """;

        return this.graphQlClient.document(document)
                .variable("projectSlug", projectSlug)
                .retrieve("project")
                .toEntity(Project.class); (1)
    }
}
1 In a Native image, we need to ensure that reflection can be performed on Project at runtime
2 @RegisterReflectionForBinding will register the relevant hints for the Project type and all types exposed as fields

9. Client

Spring for GraphQL includes client support for executing GraphQL requests over HTTP, WebSocket, and RSocket.

9.1. GraphQlClient

GraphQlClient is a contract that declares a common workflow for GraphQL requests that is independent of the underlying transport. That means requests are executed with the same API no matter what the underlying transport, and anything transport specific is configured at build time.

To create a GraphQlClient you need one of the following extensions:

Each defines a Builder with options relevant to the transport. All builders extend from a common, base GraphQlClient Builder with options relevant to all extensions.

Once you have a GraphQlClient you can begin to make requests.

9.1.1. HTTP

HttpGraphQlClient uses WebClient to execute GraphQL requests over HTTP.

WebClient webClient = ... ;
HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);

Once HttpGraphQlClient is created, you can begin to execute requests using the same API, independent of the underlying transport. If you need to change any transport specific details, use mutate() on an existing HttpGraphQlClient to create a new instance with customized settings:

WebClient webClient = ... ;

HttpGraphQlClient graphQlClient = HttpGraphQlClient.builder(webClient)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

// Perform requests with graphQlClient...

HttpGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
        .headers(headers -> headers.setBasicAuth("peter", "..."))
        .build();

// Perform requests with anotherGraphQlClient...

9.1.2. WebSocket

WebSocketGraphQlClient executes GraphQL requests over a shared WebSocket connection. It is built using the WebSocketClient from Spring WebFlux and you can create it as follows:

String url = "wss://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client).build();

In contrast to HttpGraphQlClient, the WebSocketGraphQlClient is connection oriented, which means it needs to establish a connection before making any requests. As you begin to make requests, the connection is established transparently. Alternatively, use the client’s start() method to establish the connection explicitly before any requests.

In addition to being connection-oriented, WebSocketGraphQlClient is also multiplexed. It maintains a single, shared connection for all requests. If the connection is lost, it is re-established on the next request or if start() is called again. You can also use the client’s stop() method which cancels in-progress requests, closes the connection, and rejects new requests.

Use a single WebSocketGraphQlClient instance for each server in order to have a single, shared connection for all requests to that server. Each client instance establishes its own connection and that is typically not the intent for a single server.

Once WebSocketGraphQlClient is created, you can begin to execute requests using the same API, independent of the underlying transport. If you need to change any transport specific details, use mutate() on an existing WebSocketGraphQlClient to create a new instance with customized settings:

URI url = ... ;
WebSocketClient client = ... ;

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

// Use graphQlClient...

WebSocketGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
        .headers(headers -> headers.setBasicAuth("peter", "..."))
        .build();

// Use anotherGraphQlClient...
Interceptor

The GraphQL over WebSocket protocol defines a number of connection oriented messages in addition to executing requests. For example, a client sends "connection_init" and the server responds with "connection_ack" at the start of a connection.

For WebSocket transport specific interception, you can create a WebSocketGraphQlClientInterceptor:

static class MyInterceptor implements WebSocketGraphQlClientInterceptor {

    @Override
    public Mono<Object> connectionInitPayload() {
        // ... the "connection_init" payload to send
    }

    @Override
    public Mono<Void> handleConnectionAck(Map<String, Object> ackPayload) {
        // ... the "connection_ack" payload received
    }

}

Register the above interceptor as any other GraphQlClientInterceptor and use it also to intercept GraphQL requests, but note there can be at most one interceptor of type WebSocketGraphQlClientInterceptor.

9.1.3. RSocket

RSocketGraphQlClient uses RSocketRequester to execute GraphQL requests over RSocket requests.

URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(url);

RSocketGraphQlClient client = RSocketGraphQlClient.builder()
        .clientTransport(transport)
        .build();

In contrast to HttpGraphQlClient, the RSocketGraphQlClient is connection oriented, which means it needs to establish a session before making any requests. As you begin to make requests, the session is established transparently. Alternatively, use the client’s start() method to establish the session explicitly before any requests.

RSocketGraphQlClient is also multiplexed. It maintains a single, shared session for all requests. If the session is lost, it is re-established on the next request or if start() is called again. You can also use the client’s stop() method which cancels in-progress requests, closes the session, and rejects new requests.

Use a single RSocketGraphQlClient instance for each server in order to have a single, shared session for all requests to that server. Each client instance establishes its own connection and that is typically not the intent for a single server.

Once RSocketGraphQlClient is created, you can begin to execute requests using the same API, independent of the underlying transport.

9.1.4. Builder

GraphQlClient defines a parent Builder with common configuration options for the builders of all extensions. Currently, it has lets you configure:

  • DocumentSource strategy to load the document for a request from a file

  • Interception of executed requests

9.2. Requests

Once you have a GraphQlClient, you can begin to perform requests via retrieve() or execute() where the former is only a shortcut for the latter.

9.2.1. Retrieve

The below retrieves and decodes the data for a query:

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

Mono<Project> projectMono = graphQlClient.document(document) (1)
        .retrieve("project") (2)
        .toEntity(Project.class); (3)
1 The operation to perform.
2 The path under the "data" key in the response map to decode from.
3 Decode the data at the path to the target type.

The input document is a String that could be a literal or produced through a code generated request object. You can also define documents in files and use a Document Source to resole them by file name.

The path is relative to the "data" key and uses a simple dot (".") separated notation for nested fields with optional array indices for list elements, e.g. "project.name" or "project.releases[0].version".

Decoding can result in FieldAccessException if the given path is not present, or the field value is null and has an error. FieldAccessException provides access to the response and the field:

Mono<Project> projectMono = graphQlClient.document(document)
        .retrieve("project")
        .toEntity(Project.class)
        .onErrorResume(FieldAccessException.class, ex -> {
            ClientGraphQlResponse response = ex.getResponse();
            // ...
            ClientResponseField field = ex.getField();
            // ...
        });

9.2.2. Execute

Retrieve is only a shortcut to decode from a single path in the response map. For more control, use the execute method and handle the response:

For example:

Mono<Project> projectMono = graphQlClient.document(document)
        .execute()
        .map(response -> {
            if (!response.isValid()) {
                // Request failure... (1)
            }

            ClientResponseField field = response.field("project");
            if (!field.hasValue()) {
                if (field.getError() != null) {
                    // Field failure... (2)
                }
                else {
                    // Optional field set to null... (3)
                }
            }

            return field.toEntity(Project.class); (4)
        });
1 The response does not have data, only errors
2 Field that is null and has an associated error
3 Field that was set to null by its DataFetcher
4 Decode the data at the given path

9.2.3. Document Source

The document for a request is a String that may be defined in a local variable or constant, or it may be produced through a code generated request object.

You can also create document files with extensions .graphql or .gql under "graphql-documents/" on the classpath and refer to them by file name.

For example, given a file called projectReleases.graphql in src/main/resources/graphql-documents, with content:

src/main/resources/graphql-documents/projectReleases.graphql
query projectReleases($slug: ID!) {
    project(slug: $slug) {
        name
        releases {
            version
        }
    }
}

You can then:

Mono<Project> projectMono = graphQlClient.documentName("projectReleases") (1)
        .variable("slug", "spring-framework") (2)
        .retrieve()
        .toEntity(Project.class);
1 Load the document from "projectReleases.graphql"
2 Provide variable values.

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

You can use the GraphQlClient Builder to customize the DocumentSource for loading documents by names.

9.3. Subscription Requests

GraphQlClient can execute subscriptions over transports that support it. Currently, only the WebSocket transport supports GraphQL streams, so you’ll need to create a WebSocketGraphQlClient.

9.3.1. Retrieve

To start a subscription stream, use retrieveSubscription which is similar to retrieve for a single response but returning a stream of responses, each decoded to some data:

Flux<String> greetingFlux = client.document("subscription { greetings }")
        .retrieveSubscription("greeting")
        .toEntity(String.class);

A subscription stream may end with:

  • SubscriptionErrorException if the server ends the subscription with an explicit "error" message that contains one or more GraphQL errors. The exception provides access to the GraphQL errors decoded from that message.

  • GraphQlTransportException such as WebSocketDisconnectedException if the underlying connection is closed or lost in which case you can use the retry operator to reestablish the connection and start the subscription again.

9.3.2. Execute

Retrieve is only a shortcut to decode from a single path in each response map. For more control, use the executeSubscription method and handle each response directly:

Flux<String> greetingFlux = client.document("subscription { greetings }")
        .executeSubscription()
        .map(response -> {
            if (!response.isValid()) {
                // Request failure...
            }

            ClientResponseField field = response.field("project");
            if (!field.hasValue()) {
                if (field.getError() != null) {
                    // Field failure...
                }
                else {
                    // Optional field set to null... (3)
                }
            }

            return field.toEntity(String.class)
        });

9.4. Interception

You create a GraphQlClientInterceptor to intercept all requests through a client:

static class MyInterceptor implements GraphQlClientInterceptor {

    @Override
    public Mono<ClientGraphQlResponse> intercept(ClientGraphQlRequest request, Chain chain) {
        // ...
        return chain.next(request);
    }

    @Override
    public Flux<ClientGraphQlResponse> interceptSubscription(ClientGraphQlRequest request, SubscriptionChain chain) {
        // ...
        return chain.next(request);
    }

}

Once the interceptor is created, register it through the client builder:

URI url = ... ;
WebSocketClient client = ... ;

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
        .interceptor(new MyInterceptor())
        .build();

10. Testing

Spring for GraphQL provides dedicated support for testing GraphQL requests over HTTP, WebSocket, and RSocket, as well as for testing directly against a server.

To make use of this, add spring-graphql-test to your build:

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

10.1. GraphQlTester

GraphQlTester is a contract that declares a common workflow for testing GraphQL requests that is independent of the underlying transport. That means requests are tested with the same API no matter what the underlying transport, and anything transport specific is configured at build time.

To create a GraphQlTester that performs requests through a client, you need one of the following extensions:

To create a GraphQlTester that performs tests on the server side, without a client:

Each defines a Builder with options relevant to the transport. All builders extend from a common, base GraphQlTester Builder with options relevant to all extensions.

10.1.1. HTTP

HttpGraphQlTester uses WebTestClient to execute GraphQL requests over HTTP, with or without a live server, depending on how WebTestClient is configured.

To test in Spring WebFlux, without a live server, point to your Spring configuration that declares the GraphQL HTTP endpoint:

ApplicationContext context = ... ;

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

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

To test in Spring MVC, without a live server, do the same using MockMvcWebTestClient:

ApplicationContext context = ... ;

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

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

Or to test against a live server running on a port:

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

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

Once HttpGraphQlTester is created, you can begin to execute requests using the same API, independent of the underlying transport. If you need to change any transport specific details, use mutate() on an existing HttpSocketGraphQlTester to create a new instance with customized settings:

HttpGraphQlTester tester = HttpGraphQlTester.builder(clientBuilder)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

// Use tester...

HttpGraphQlTester anotherTester = tester.mutate()
        .headers(headers -> headers.setBasicAuth("peter", "..."))
        .build();

// Use anotherTester...

10.1.2. WebSocket

WebSocketGraphQlTester executes GraphQL requests over a shared WebSocket connection. It is built using the WebSocketClient from Spring WebFlux and you can create it as follows:

String url = "http://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client).build();

WebSocketGraphQlTester is connection oriented and multiplexed. Each instance establishes its own single, shared connection for all requests. Typically, you’ll want to use a single instance only per server.

Once WebSocketGraphQlTester is created, you can begin to execute requests using the same API, independent of the underlying transport. If you need to change any transport specific details, use mutate() on an existing WebSocketGraphQlTester to create a new instance with customized settings:

URI url = ... ;
WebSocketClient client = ... ;

WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

// Use tester...

WebSocketGraphQlTester anotherTester = tester.mutate()
        .headers(headers -> headers.setBasicAuth("peter", "..."))
        .build();

// Use anotherTester...

WebSocketGraphQlTester provides a stop() method that you can use to have the WebSocket connection closed, e.g. after a test runs.

10.1.3. RSocket

RSocketGraphQlTester uses RSocketRequester from spring-messaging to execute GraphQL requests over RSocket:

URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(url);

RSocketGraphQlTester client = RSocketGraphQlTester.builder()
        .clientTransport(transport)
        .build();

RSocketGraphQlTester is connection oriented and multiplexed. Each instance establishes its own single, shared session for all requests. Typically, you’ll want to use a single instance only per server. You can use the stop() method on the tester to close the session explicitly.

Once RSocketGraphQlTester is created, you can begin to execute requests using the same API, independent of the underlying transport.

10.1.4. GraphQlService

Many times it’s enough to test GraphQL requests on the server side, without the use of a client to send requests over a transport protocol. To test directly against a ExecutionGraphQlService, use the ExecutionGraphQlServiceTester extension:

GraphQlService service = ... ;
ExecutionGraphQlServiceTester tester = ExecutionGraphQlServiceTester.create(service);

Once ExecutionGraphQlServiceTester is created, you can begin to execute requests using the same API, independent of the underlying transport.

ExecutionGraphQlServiceTester.Builder provides an option to customize ExecutionInput details:

GraphQlService service = ... ;
ExecutionGraphQlServiceTester tester = ExecutionGraphQlServiceTester.builder(service)
        .configureExecutionInput((executionInput, builder) -> builder.executionId(id).build())
        .build();

10.1.5. WebGraphQlHandler

The GraphQlService extension lets you test on the server side, without a client. However, in some cases it’s useful to involve server side transport handling with given mock transport input.

The WebGraphQlTester extension lets you processes request through the WebGraphQlInterceptor chain before handing off to ExecutionGraphQlService for request execution:

WebGraphQlHandler handler = ... ;
WebGraphQlTester tester = WebGraphQlTester.create(handler);

The builder for this extension allows you to define HTTP request details:

WebGraphQlHandler handler = ... ;

WebGraphQlTester tester = WebGraphQlTester.builder(handler)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

Once WebGraphQlServiceTester is created, you can begin to execute requests using the same API, independent of the underlying transport.

10.1.6. Builder

GraphQlTester defines a parent Builder with common configuration options for the builders of all extensions. It lets you configure the following:

  • errorFilter - a predicate to suppress expected errors, so you can inspect the data of the response.

  • documentSource - a strategy for loading the document for a request from a file on the classpath or from anywhere else.

  • responseTimeout - how long to wait for request execution to complete before timing out.

10.2. Requests

Once you have a GraphQlTester, you can begin to test requests. The below executes a query for a project and uses JsonPath to extract project release versions from the response:

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

graphQlTester.document(document)
        .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 document files with extensions .graphql or .gql under "graphql-test/" on the classpath and refer to them by file name.

For example, given a file called projectReleases.graphql in src/main/resources/graphql-test, with content:

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

You can then use:

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

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

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();

See Errors for more details on error handling.

10.3. Subscriptions

To test subscriptions, call executeSubscription instead of execute to obtain a stream of responses and then use StepVerifier from Project Reactor to inspect the stream:

Flux<String> greetingFlux = tester.document("subscription { greetings }")
        .executeSubscription()
        .toFlux("greetings", String.class);  // decode at JSONPath

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

Subscriptions are supported only with WebSocketGraphQlTester, or with the server side GraphQlService and WebGraphQlHandler extensions.

10.4. Errors

When you use verify(), any errors under the "errors" key in the response will cause an assertion failure. To suppress a specific error, use the error filter before verify():

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

You can register an error filter at the builder level, to apply to all tests:

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

If you want to verify that an error does exist, and in contrast to filter, throw an assertion error if it doesn’t, then use exepect instead:

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

You can also inspect all errors through a Consumer, and doing so also marks them as filtered, so you can then also inspect the data in the response:

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

11. Boot Starter

Spring Boot provides a starter for building GraphQL applications with Spring for GraphQL. For version information, see the Spring for GraphQL Versions wiki page.

The easiest way to get started is via start.spring.io by selecting "Spring for GraphQL" along with an underlying transport such as Spring MVC of WebFlux over HTTP or WebSocket, or over RSocket. Refer to the Spring for GraphQL Starter section in the Spring Boot reference for details on supported transports, auto-configuration related features, and more. For testing support, see Auto-Configured GraphQL Tests.

For further reference, check the following GraphQL related:

12. Samples

Check out the "Building a GraphQL service" Getting Started Guide.

In addition, the 1.0.x branch of this repository contains sample applications for various scenarios. Those samples do not exist in the main branch and are planned to be moved out into a separate repository. To run those samples, check out the 1.0.x branch run their main application classes from your IDE, or from the command line:

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