Skip to content

3. Advanced Configuration

Balf edited this page May 6, 2024 · 2 revisions

Advanced configuration

Using Gson rather than Jackson

GraphQL SPQR can work with both Jackson and Gson. SPQR first checks for Jackson on the classpath and prefers that if it's available. If not, it checks for Gson, and uses that. So additional config is only needed if both libraries are available but Gson is preferred:

//Updated from Getting Started
GraphQLSchema schema = new GraphQLSchemaGenerator()
        .withBasePackages("io.leangen") 
        .withOperationsFromSingleton(userService) 
        .withValueMapperFactory(new GsonValueMapperFactory())
        .generate();

Influencing GraphQL SPQR's type generation

GraphQL SPQR support a vast list of methods to influence the way the GraphQL Schema is generated. This allow you to dynamically alter any GraphQL Types that are generated by GraphQL SPQR.

Modifying generated GraphQL Types

In some cases the GraphQL type generated from you Domain class may need additional fields that aren't available in the Domain class. There are 2 ways you can add fields to an existing GraphQL type without having to modify the Domain class.

Using the @GraphQLContext annotation

If you wish to extend your GraphQL Type with an additional field the easiest way to do this is to use the @GraphQLContext annotation.

Let's say we want to extend the User type from the Getting Started with a new field "twitterProfile", which returns a TwitterProfile type. First we need the TwitterProfile type.

class TwitterProfile {
    
    @GraphQLQuery
    public String getHandle() {
        ...
    }
    
    @GraphQLQuery
    public int getNumberOfTweets() {
        ...
    }
    
}

Now we have to let SPQR know that this TwitterProfile is a part of the User type. We can do this using the @GraphQLContext annotation. We update the UserService with the following method:

class UserService {
    //existing queries
    
    public TwitterProfile getTwitterProfile(@GraphQLContext User user) {
        ...
    }
}

And that's all there is to it. Now you can query the TwitterProfile like so:

ExecutionResult result = graphQL.execute(
        "{ user (id: 123) {
        name,
        regDate,
        twitterProfile {
handle
        numberOfTweets
         }
                 }}");

Using a TypeMapper

If the @GraphQLContext method is not sufficient for your use case, for example if the additional fields are configured rather than coded, you can use a TypeMapper. SPQR uses TypeMappers to transform Java classes to GraphQL Types. By creating your own TypeMapper you can influence the resulting GraphQL Type.

The easiest way to achieve this is by extending SPQR's default ObjectTypeMapper and override the getFields() and supports() methods.

public class CustomTypeMapper extends ObjectTypeMapper {

    @Override
    protected List<GraphQLFieldDefinition> getFields(String typeName, AnnotatedType javaType, TypeMappingEnvironment env) {
        
        //Retrieve the fields from the annotated methods by calling the super#getFields() method. 
        List<GraphQLFieldDefinition> fields = super.getFields(typeName, javaType, env);
        
        //Now add a custom field
        fields.add(GraphQLFieldDefinition.newFieldDefinition() 
                .name("twitterHandle")
                .type(GraphQLString)
                .build());
        
        // Append a custom DataFetcher (also known as a Resolver)
        DataFetcher<?> dataFetcher = e -> {
            User user = (User) e.getSource(); 
            
            return getTwitterHandleForUser(user);
        };
        // And register the DataFetcher to the code registry
        env.buildContext.codeRegistry.dataFetcher(coordinates(typeName, "twitterHandle"), dataFetcher);
        return fields;
    }

    @Override
    public boolean supports(AnnotatedElement element, AnnotatedType type) {
        return ClassUtils.getRawType(type).equals(User.class);
    }
}

There are a couple of things happening in the code above:

The supports method checks what kind of class is provided and will return true if the proved AnnotatedType is a User class. This makes sure that the getFields() method is only called for the User class.

When the getFIelds() method is called it will initially call the ObjectTypeMapper's getFields() method to retrieve all fields based on the Java type, as is SPQR's default behavior. Afterward a new GraphQLFieldDefinition is created to represent the twitterHandle field, and it's added to the list of fields generated by the ObjectTypeMapper.

Although the field is now attached to the User object, GraphQL does not know where to get the data for that field from. To resolve this we need to provide the GraphQL Schema with a DataFetcher, or Resolver as they are sometimes called, to retrieve the data for the twitterHandle field.

The DataFetcher then needs to be registered to the code registry, which keeps track of all DataFetchers and TypeResolver within the Schema. When registering the DataFetcher you will need to provide the coordinates to which this DataFetcher applies, meaning the name of the type (User) in combination with the name of the field (twitterProfile).

When you've created your TypeMapper you need to let SPQR know that this TypeMapper should be used. Fortunately that is very easy and can be done like this:

GraphQLSchema schema = new GraphQLSchemaGenerator()
        .withBasePackages("io.leangen") //not mandatory but strongly recommended to set your "root" packages
        .withOperationsFromSingleton(userService) //register the service
        .withTypeMappers(new CustomTypeMapper())
        .generate(); //done ;)

For more information on DataFetchers please refer to the GraphQL Java documentation.