Repository navigation
How graphql java tools builds a schema
graphql-java-tools turns a GraphQL schema written in SDL, plus your own Java or Kotlin classes, into an executable graphql-java
GraphQLSchema. You don't write data fetchers or type wiring. The library finds the class that backs each GraphQL type and the
method or property that backs each field, then generates the data fetchers for you. All of this happens once, when you build the
schema. A field with no matching method, or a type used with two different classes, fails at startup instead of at query time.
The rest of this page follows one small schema through each step:
type Query {
books: [Book!]!
}
type Book {
title: String!
author: Author!
}
type Author {
name: String!
}class Query : GraphQLQueryResolver {
fun books(): List<Book> = bookRepository.findAll()
}
class BookResolver : GraphQLResolver<Book> {
fun author(book: Book): Author = authorRepository.find(book.authorId)
}
data class Book(val title: String, val authorId: Long)
data class Author(val name: String)
val schema = SchemaParser.newParser()
.file("schema.graphqls")
.resolvers(Query(), BookResolver())
.build()
.makeExecutableSchema()SchemaParserBuilder reads each .file(...) from the classpath and each .schemaString(...), and parses them with graphql-java's
parser. Each TypeDefinitionFactory in SchemaParserOptions can then add definitions. The default one, RelayConnectionFactory,
generates the Connection and Edge types for fields marked @connection.
SchemaClassScanner discovers classes by following method signatures, so it needs a class to start from. The root resolvers are
that start. The scanner reads the root type names from the schema { ... } block, or defaults to Query, Mutation, and
Subscription. Resolvers that implement GraphQLQueryResolver, GraphQLMutationResolver, or GraphQLSubscriptionResolver back
those types. Every other resolver implements GraphQLResolver<T> and adds fields to the GraphQL type whose class is T.
For each field of a type, FieldResolverScanner looks for a method that can produce its value. It searches the resolvers for the
type's class first, then the class itself. For Book.author, it searches BookResolver, then Book.
A method on a GraphQLResolver takes the source object as its first parameter, as in author(book: Book). A method on the class
itself does not. The next parameters match the field's GraphQL arguments, in order. A method can end with a
DataFetchingEnvironment, a GraphQLContext, or the configured contextClass.
For a field named title, the scanner tries these method names in order:
title-
isTitle, only when the field is aBoolean getTitlegetFieldTitle-
getplus the camelCase form of a snake_case name, so the fieldkey_opsmatchesgetKeyOps
If no method matches and the field has no arguments, the scanner looks for a Java field of the same name. A class that
implements Map resolves every field by key. If nothing matches, the build fails with a FieldResolverError that lists each
signature it tried. A root field that matches on more than one root resolver also fails the build.
Each field resolver from step 3 pairs Java types with GraphQL types. The method's return type pairs with the field's type, and each
parameter type pairs with the type of its argument. TypeClassMatcher unwraps both sides of a pair until a named GraphQL type
faces a single class:
- On the Java side, it unwraps
CompletableFuture,CompletionStage,Future,Publisher,DataFetcherResult,Optional, and any generic wrapper you register inSchemaParserOptions. It also resolves type variables and boxes primitives. - On the GraphQL side, it drops
!. A list type[T]needs anIterableor an array, and the element type then pairs withT.
books(): List<Book> against [Book!]! gives the pair Book and Book. The scanner records each pair in its dictionary. If a
GraphQL type turns up with two different classes, the build fails with a SchemaClassScannerError that names both classes and where
each came from. Scalars don't get a class. The scanner only checks that you passed a GraphQLScalarType for each custom
scalar.
Each newly matched object type goes into a queue. Scanning it runs step 3 on its fields, which can match more types, which join the
queue in turn. In the example, Query leads to Book, and BookResolver.author leads to Author. Each newly matched input type
gets its fields matched against the properties of its class.
Some classes never appear in a method signature. A field that returns an interface is backed by a method that returns the Java
interface, so the classes of the implementing types stay unknown. The members of a union have the same problem. Pass those classes
with .dictionary(...), or the build fails and names the type. For a Kotlin sealed class, the scanner finds the subclass whose name
matches the GraphQL type on its own.
Once the queue is empty, the scanner logs a WARN for each schema type that no field can reach, each dictionary entry it never used, and each resolver whose methods back no field. A custom scalar you didn't pass fails the build.
SchemaParser.makeExecutableSchema() creates the graphql-java types and registers a data fetcher for every field. Each interface
and union gets a type resolver. At query time, that type resolver looks up the class of the returned object in the dictionary,
then its superclasses and interfaces, and falls back to the class's simple name. Directive wirings run last, on the assembled
types.
Each data fetcher calls the method or reads the property that step 3 found. For a method on a GraphQLResolver, it passes the
source object first. It converts each argument to its parameter type with Jackson, then appends the environment or the context.
It runs a Kotlin suspend function as a coroutine and returns a CompletableFuture. graphql-java waits on the futures that
resolvers return, so you can mix blocking and async methods in one schema.