Spring for GraphQL : @SchemaMapping and @QueryMapping

In the last article Getting started with Spring Boot GraphQL service, we discussed features of GraphQL and its implementation in Spring for GraphQL using low-level APIs of GraphQL Java.

While low-level GraphQL Java implementation is fine for understanding the concept, you’ll mostly use higher-level abstraction provided by Spring.

In this article, we will discuss the @Controller, @QueryMapping, and @SchemaMapping of Spring for GraphQL.

Here, we’ll use the same book catalog GraphQL API, described in an earlier article (also shown below).


type Query {
    books: [Book]
    bookById(id : ID) : Book
}

type Book {
    id : ID
    name : String
    author: String
    price: Float
    ratings: [Rating]
}

type Rating {
    id: ID
    rating: Int
    comment: String
    user: String
}

Let’s get started!

Controller

Like REST API, Spring Boot offers @Controller annotation to implement GraphQL APIs. All you need to do is to annotate your controller class with @Controller annotation and define either @QueryMapping or @SchemaMapping annotation on the handler method.

Spring detects @Controller beans and registers their annotated handler methods ( @QueryMapping or @SchemaMapping) as DataFetchers (also known as a resolver).

resolver/DataFetcher is a function that’s responsible for populating the data for a single field in your GraphQL schema.

SchemaMapping Annotation

The @SchemaMapping annotation maps a handler method to a field in the GraphQL schema as DataFetcher. All you need to do is to define the typeName and, optionally, the field with @SchemaMapping.


@SchemaMapping(typeName = "Query", field = "books")
public Collection<Book> books() {
  return bookCatalogService.getBooks();
}

Additionally, if you keep the method name as same as the field name then you can omit the field from @SchemaMapping.


@SchemaMapping(typeName = "Query")
public Collection<Book> books() {
  return bookCatalogService.getBooks();
}


What happens if you don’t specify the typeName with @SchemaMapping (as shown below) ?


@SchemaMapping()
public Collection<Book> books() {
  return bookCatalogService.getBooks();
}

In this case, Spring can’t figure out which parent-type field books belong to; therefore, it’ll throw BeanCreationException, at the application startup, with a helpful error message.


No parentType specified, and a source/parent method argument was also not found: BooksCatalogController#books[0 args]

Similarly, for the field bookById, you can define a data fetcher as:


@SchemaMapping(typeName = "Query")
public Book bookById(@Argument("id") Integer id) {
  return bookCatalogService.bookById(id);
}

Here, we have used @Argument annotation to map argument id as @Argument("id") Integer id.

Finally, you can test this by heading over to http://localhost:8080/graphiql?path=/graphql in your browser.

Query: books
Query: bookById

Data Fetcher for the child ‘Type’ field

If you query GraphQL service with the ratings field of Book the response includes "ratings": null?

Why?

If you don’t specify a Data Fetcher for a field the GraphQL Java assigns a default PropertyDataFetcher. Subsequently, at the runtime this PropertyDataFetcher looks for public getRatings() field in the Book POJO (as field ratings belongs to the Book).

To solve this, you can define a Data Fetcher for ratings. As the field ratings belongs to parent type Book, you need to pass Book in the argument.


@SchemaMapping(typeName = "Book", field = "ratings")
public List<Rating> ratings(Book book) {
  return bookCatalogService.ratings(book);
}

While GraphQL Java is executing a query, it calls the appropriate DataFetcher for each field it encounters in the query. If you don’t define any DataFetcher it assigns PropertyDataFetcher as default.

And, you can write without specifying the field as:


@SchemaMapping(typeName = "Book")
public List<Rating> ratings(Book book) {
  return bookCatalogService.ratings(book);
}

Also as:


@SchemaMapping
public List<Rating> ratings(Book book) {
  return bookCatalogService.ratings(book);
}

In @SchemaMapping you can also leave out typeName and field 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.

Query: bookById with ratings field

QueryMapping annotation

Instead of using @SchemaMapping, you can use @QueryMapping. Effectively, this is a shortcut annotation to bind annotated methods for fields under the Query type.


@QueryMapping(name = "books")
public Collection<Book> books() {
  return bookCatalogService.getBooks();
}

Moreover, you can skip the name from annotation. In that case, Spring derives the query field from the method name.


@QueryMapping
public Collection<Book> books() {
  return bookCatalogService.getBooks();
}

Similarly, for the Query field bookById:


@QueryMapping
public Book bookById(@Argument Integer id) {
  return bookCatalogService.bookById(id);
}

You can’t use @QueryMapping for the field ratings as it doesn’t belong to the type Query.

Configuration

By default, the Boot starter checks in src/main/resources/graphql for GraphQL schema files with extensions “.graphqls” or “.gqls“. To override this behavior, you can change the following property.

spring.graphql.schema.locations=classpath:graphql/ 
spring.graphql.schema.fileExtensions=.graphqls, .gqls

Code example

The working code example of this article is listed on GitHub  . To run the example, clone the repository, and import graphql-spring-annotation as a project in your favorite IDE as a Gradle project.

Summary

Spring greatly simplifies the development of GraphQL APIs by the way of @Controller annotation. You just need to annotate your Controller classes with @Controller annotation and define either @QueryMapping or @SchemaMapping annotation on the handler method.

Spring detects @Controller beans and registers their annotated handler methods as DataFetchers (also known as a resolver).

Social Share !
Pankaj
Pankaj

Software Architect @ Schlumberger ``` Cloud | Microservices | Programming | Kubernetes | Architecture | Machine Learning | Java | Python ```