This utility library helps to generate request body for GraphQL queries using POJOs.
To add library to your project perform next steps:
Add the following dependency to your pom.xml
:
<dependency>
<groupId>com.github.vladislavsevruk</groupId>
<artifactId>graphql-request-body-generator</artifactId>
<version>1.0.16</version>
</dependency>
Add the following dependency to your build.gradle
:
implementation 'com.github.vladislavsevruk:graphql-request-body-generator:1.0.16'
First of all we need to choose field marking strategy that will be used for GraphQL operation generation.
There are two predefined field marking strategy that define the way how builder determine if field should be used for GraphQL operation generation:
- Use all fields except ones with GqlIgnore annotation [default]
- Use only fields with one of GqlField or GqlDelegate annotations
Current strategy for both operation selection set and mutation input object values can be set using FieldMarkingStrategySourceManager:
FieldMarkingStrategySourceManager.selectionSet().useAllExceptIgnoredFieldsStrategy();
// or
FieldMarkingStrategySourceManager.input().useOnlyMarkedFieldsStrategy();
However, you can set your own custom FieldMarkingStrategy:
FieldMarkingStrategySourceManager.selectionSet().useCustomStrategy(field -> true);
Then we need to prepare POJO models that will be used for GraphQL operation generation according to chosen field marking strategy.
For automatic generation of POJO models at your project you can use following options:
Add the following plugin to your build.gradle
:
plugins {
id 'io.github.vladislavsevruk.graphql-model-generator-plugin' version '1.0.0'
}
This plugin will add additional generateGraphqlModels
task before javaCompile
that will automatically generate
POJO models based on GraphQL schema file. Generation results can be customized using followed options of
graphqlModelGenerator
extension:
import com.github.vladislavsevruk.generator.model.graphql.constant.*
graphqlModelGenerator {
addJacksonAnnotations = false
entitiesPrefix = ''
entitiesPostfix = ''
pathToSchemaFile = '/path/to/schema.graphqls'
targetPackage = 'com.myorg'
treatArrayAs = ElementSequence.LIST
treatFloatAs = GqlFloatType.DOUBLE
treatIdAs = GqlIntType.STRING
treatIntAs = GqlIntType.INTEGER
updateNamesToJavaStyle = true
useLombokAnnotations = false
usePrimitivesInsteadOfWrappers = false
useStringsInsteadOfEnums = false
}
- addJacksonAnnotations reflects if jackson annotations should be added to fields for proper mapping using Jackson
library. Default value is
false
; - entitiesPrefix is used for adding specific prefix to generated POJO model names. Default value is empty string;
- entitiesPostfix is used for adding specific postfix to generated POJO model names. Default value is empty string;
- pathToSchemaFile is used for setting location of GraphQL schema file. Default location is
src/main/resources/graphql/schema.graphqls
; - targetPackage is used for setting specific package name for generated POJO models. Default value is
com.github.vladislavsevruk.model
; - treatArrayAs is used for setting entity that should be used for GraphQL array type generation. Can be one of
- following values:
ARRAY
,COLLECTION
,ITERABLE
,LIST
,SET
. Default value isLIST
; - treatFloatAs is used for setting entity that should be used for GraphQL array type generation. Can be one of
- following values:
BIG_DECIMAL
,DOUBLE
,FLOAT
,STRING
. Default value isDOUBLE
; - treatIdAs is used for setting entity that should be used for GraphQL array type generation. Can be one of
- following values:
BIG_INTEGER
,INTEGER
,LONG
,STRING
. Default value isSTRING
; - treatIntAs is used for setting entity that should be used for GraphQL array type generation. Can be one of
- following values:
BIG_INTEGER
,INTEGER
,LONG
,STRING
. Default value isINTEGER
; - updateNamesToJavaStyle reflects if entities and fields from GraphQL schema should be updated to follow default
java convention (upper camel case for classes and lower camel case for fields). Default value is
true
; - useLombokAnnotations reflects if lombok annotations should be used for POJO methods generation instead of
ones generated by this plugin. Default value is
false
; - usePrimitivesInsteadOfWrappers reflects if java primitives should be used for GraphQL scalar types instead of
java primitive wrapper classes. Default value is
false
; - useStringsInsteadOfEnums reflects if GraphQL enum fields at type entities should be converted to string type.
Default value is
false
;
However, you still can create and customize model by yourself following rules described below.
Selection set generation is based only on fields that are declared at class or it superclasses and doesn't involve declared methods.
GqlField annotation marks model fields that should be treated as GraphQL field:
public class User {
@GqlField
private Long id;
}
By default field name will be used for generation but you can override it using name method:
public class User {
@GqlField(name = "wishListItemsUrls")
private List<String> wishListItems;
}
Some fields may contain a selection set so if you mark field using withSelectionSet method it will be treated as field that have nested fields:
public class User {
@GqlField(withSelectionSet = true)
private Contacts contacts;
@GqlField(withSelectionSet = true)
private List<Order> orders;
}
public class Contacts {
@GqlField
private String email;
@GqlField
private String phoneNumber;
}
public class Order {
@GqlField
private Long id;
@GqlField
private Boolean isDelivered;
}
Annotation also has method nonNull that allow to mark field that was denoted as non-null at GraphQL schema:
public class User {
@GqlField(nonNull = true)
private Long id;
}
If field requires arguments for GraphQL operation they can be provided via arguments function:
public class User {
@GqlField(arguments = { @GqlFieldArgument(name = "width", value = "100"),
@GqlFieldArgument(name = "height", value = "50") })
private String profilePic;
}
Values of GqlFieldArgument annotation will be added to selection set "as is" so you may need to escape quotes for literal values:
public class User {
@GqlField(withSelectionSet = true,
arguments = @GqlFieldArgument(name = "sortBy", value = "\"orderDate DESC\""))
private List<Order> orders;
}
public class Order {
@GqlField
private Long id;
@GqlField
private Date orderDate;
}
Also alias can be specified for field using self-titled method:
public class User {
@GqlField(name = "profilePic",
alias = "smallPic",
arguments = { @GqlFieldArgument(name = "size", value = "64")})
private String smallPic;
@GqlField(name = "profilePic",
alias = "bigPic",
arguments = { @GqlFieldArgument(name = "size", value = "1024")})
private String bigPic;
}
GqlDelegate is used for complex fields to treat its inner fields like they are declared at same class where field itself declared:
public class User {
@GqlDelegate
private UserInfo userInfo;
}
public class UserInfo {
@GqlField
private String firstName;
@GqlField
private String lastName;
}
Code above is equivalent to:
public class User {
@GqlField
private String firstName;
@GqlField
private String lastName;
}
GqlUnion annotation is used for fields that should be treated as union:
public class Order {
@GqlField
private Long id;
@GqlField
private Date orderDate;
@GqlUnion({ @GqlUnionType(Book.class), @GqlUnionType(value = BoardGame.class, name = "Game") })
private OrderItem orderItem;
}
public class OrderItem {
@GqlField
private Long id;
@GqlField
private String title;
}
public class Book extends OrderItem {
@GqlField
private Long numberOfPages;
}
public class BoardGame extends OrderItem {
@GqlField
private Long numberOfPlayers;
}
GqlIgnore is used with "all fields except ignored" field marking strategy for marking field that shouldn't be used for generation:
public class UserInfo {
private String firstName;
@GqlIgnore
private String fullName;
private String lastName;
}
Input object value generation goes through fields that are declared at class or it superclasses and gets values from related getter methods (or field itself if no related getter found).
GqlField annotation is also used for input object value generation and marks input values of any type:
public class User {
@GqlField
private Long id;
@GqlField
private Contacts contacts;
}
public class Contacts {
@GqlField
private String email;
@GqlField
private String phoneNumber;
}
By default field name will be used for generation but you can override it using name method:
public class User {
@GqlField(name = "wishListItemsUrls")
private List<String> wishListItems;
}
GqlInput annotation is used with methods and helps to point to related GraphQL field if method name doesn't match field getter pattern:
public class User {
@GqlField
private String name;
@GqlField
private String surname;
@GqlInput(name = "name")
public String getFirstName() {
return name;
}
@GqlInput(name = "surname")
public String getLastName() {
return surname;
}
}
If provided name doesn't match any of GraphQL fields at model result of method execution will be treated as new field:
public class User {
@GqlField
private String firstName;
@GqlField
private String lastName;
@GqlInput(name = "fullName")
public String getFullName() {
return firstName + " " + lastName;
}
}
GqlDelegate is used for complex values to treat its inner fields like they are declared at same class where field itself declared:
public class User {
@GqlDelegate
private UserInfo userInfo;
}
public class UserInfo {
@GqlField
private String firstName;
@GqlField
private String lastName;
}
Models above are equivalent to:
public class User {
@GqlField
private String firstName;
@GqlField
private String lastName;
}
Like GqlInput GqlDelegate can be applied to methods without related GraphQL field. In this case return value of method will be treated as delegated field:
public class User {
@GqlDelegate
public UserInfo getUserInfo() {
// UserInfo generation
}
}
public class UserInfo {
@GqlField
private String firstName;
@GqlField
private String lastName;
}
Code above is equivalent to:
public class User {
@GqlField
private String firstName;
@GqlField
private String lastName;
}
GqlIgnore is used with "all fields except ignored" field marking strategy for marking field that shouldn't be used for generation:
public class UserInfo {
private String firstName;
@GqlIgnore
private String fullName;
private String lastName;
}
Once POJO models are ready we can generate GraphQL operation using GqlRequestBodyGenerator:
// query
String query = GqlRequestBodyGenerator.query("allUsers").selectionSet(User.class).generate();
// prepare input model
User newUser = new User();
newUser.setFirstName("John");
newUser.setLastName("Doe");
// mutation
GqlInputArgument input = GqlInputArgument.of(newUser);
String mutation = GqlRequestBodyGenerator.mutation("newUser").arguments(input).selectionSet(User.class)
.generate();
Generated operation is wrapped into json and can be passed as body to any API Client. If you want to generate pure
unwrapped to JSON GraphQL query you can use GqlRequestBodyGenerator.unwrapped()
first, all other methods calls
remain the same:
// query
String query = GqlRequestBodyGenerator.unwrapped().query("allUsers").selectionSet(User.class)
.generate();
// prepare input model
User newUser = new User();
newUser.setFirstName("John");
newUser.setLastName("Doe");
// mutation
GqlInputArgument input = GqlInputArgument.of(newUser);
String mutation = GqlRequestBodyGenerator.unwrapped().mutation("newUser").arguments(input)
.selectionSet(User.class).generate();
By default, all marked fields will be used for selection set generation. However, you can generate selection set using one of predefined fields picking strategies:
- pick all marked fields [default]
String query = GqlRequestBodyGenerator.query("allUsers")
.selectionSet(User.class, SelectionSetGenerationStrategy.allFields()).generate();
// same result as
String query2 = GqlRequestBodyGenerator.query("allUsers").selectionSet(User.class).generate();
- pick only fields with id name or fields that have nested field with id name
String query = GqlRequestBodyGenerator.query("allUsers")
.selectionSet(User.class, SelectionSetGenerationStrategy.onlyId()).generate();
- pick only fields that are marked as non-null
String query = GqlRequestBodyGenerator.query("allUsers")
.selectionSet(User.class, SelectionSetGenerationStrategy.onlyNonNull()).generate();
- pick all except fields with selection set
String query = GqlRequestBodyGenerator.query("allUsers")
.selectionSet(User.class, SelectionSetGenerationStrategy.fieldsWithoutSelectionSets()).generate();
Also you can provide your own custom fields picking strategy that implements FieldsPickingStrategy functional interface:
String query = GqlRequestBodyGenerator.query("allUsers")
.selectionSet(User.class, field -> field.getName().contains("Name")).generate();
If you want to use generic models it's recommended to provide them via TypeProvider:
String query = GqlRequestBodyGenerator.query("allUsers")
.selectionSet(new TypeProvider<User<UserInfo>>() {}).generate();
Some models may contain circular type reference on each other, like
public class Parent {
@GqlField
private Long id;
@GqlField(withSelectionSet = true)
private List<Child> children;
}
public class Child {
@GqlField
private Long id;
@GqlField(withSelectionSet = true)
private Parent parent;
}
which leads to stack overflow during selection set generation. To avoid this library uses loop breaking mechanism that can be customized using one of predefined loop breaking strategies:
- do not include detected looped item to selection set [default]
String query = GqlRequestBodyGenerator.query("queryName").selectionSet(Parent.class)
.generate();
will result to
{"query":"{queryName{id children{id}}}"}
- allow received nesting level
int nestingLevel = 1;
String query = GqlRequestBodyGenerator.query("queryName")
.selectionSet(Parent.class, EndlessLoopBreakingStrategy.nestingStrategy(nestingLevel))
.generate();
will result to
{"query":"{queryName{id children{id parent{id children{id}}}}}"}
This strategy will be used as default during generation but you can set another max nesting level for specific field
using maxNestingLoopLevel
method at GqlField and GqlUnionType annotations.
public class Parent {
@GqlField
private Long id;
@GqlField(withSelectionSet = true, maxNestingLoopLevel = 1)
private List<Child> children;
}
public class Child {
@GqlField
private Long id;
@GqlField(withSelectionSet = true)
private Parent parent;
}
Also you can provide your own custom loop breaking strategy that implements LoopBreakingStrategy functional interface:
String query = GqlRequestBodyGenerator.query("queryName")
.selectionSet(Parent.class,
(typeMeta, trace) -> typeMeta.getType().equals(Parent.class))
.generate();
Some operations may require arguments (to pick specific item or filter items list, for example) so you can provide necessary arguments to operation using GqlArgument class:
GqlArgument<Long> idArgument = GqlArgument.of("id", 1L);
String query = GqlRequestBodyGenerator.query("user").arguments(idArgument)
.selectionSet(User.class).generate();
If you need to provide several arguments you can use varargs:
GqlArgument<List<String>> firstNameArgument = GqlArgument.of("lastName", Arrays.asList("John", "Jane"));
GqlArgument<String> lastNameArgument = GqlArgument.of("lastName", "Doe");
String query = GqlRequestBodyGenerator.query("activeUsers").arguments(firstNameArgument, lastNameArgument)
.selectionSet(User.class).generate();
or iterables:
GqlArgument<List<String>> firstNameArgument = GqlArgument.of("lastName", Arrays.asList("John", "Jane"));
GqlArgument<String> lastNameArgument = GqlArgument.of("lastName", "Doe");
List<GqlArgument<?>> arguments = Arrays.asList(firstNameArgument, lastNameArgument);
String query = GqlRequestBodyGenerator.query("activeUsers").arguments(arguments)
.selectionSet(User.class).generate();
Mutations usually use input argument to pass complex input objects. You can use GqlInputArgument to pass input object for mutation:
// prepare input model
User newUser = new User();
newUser.setFirstName("John");
newUser.setLastName("Doe");
// mutation
GqlInputArgument<User> inputArgument = GqlInputArgument.of(newUser);
// the same as GqlArgument<User> inputArgument = GqlArgument.of("input", newUser);
String mutation = GqlRequestBodyGenerator.mutation("newUser").arguments(inputArgument)
.selectionSet(User.class).generate();
By default, input object will be generated using non-null field values but like selection set input object can be generated using predefined fields picking strategies:
- pick all marked fields
String query = GqlRequestBodyGenerator.mutation("newUser")
.arguments(InputGenerationStrategy.allFields(), inputArgument).selectionSet(User.class).generate();
- pick only fields with non-null value
String query = GqlRequestBodyGenerator.mutation("newUser")
.arguments(InputGenerationStrategy.nonNullsFields(), inputArgument).selectionSet(User.class).generate();
But you can provide your own input fields picking strategy that implements InputFieldsPickingStrategy interface:
InputFieldsPickingStrategy inputFieldsPickingStrategy = field -> field.getName().contains("Name");
String query = GqlRequestBodyGenerator.mutation("newUser")
.arguments(inputFieldsPickingStrategy, inputArgument).selectionSet(User.class).generate();
As GraphQL query can be parameterized with variables you can generate GraphQL operation body that way passing values as operation arguments and using one of predefined variables generation strategies:
- generate only for arguments of GqlVariableArgument type:
String variableName = "id";
GqlParameterValue<Integer> variable = GqlVariableArgument.of(variableName, variableName, 1, true);
String query = GqlRequestBodyGenerator.query("getProfile")
.arguments(VariableGenerationStrategy.byArgumentType(), variable)
.selectionSet(User.class).generate();
- generate only for arguments with value type annotated by GqlVariableType type;
@GqlVariableType(variableType = "ContactsData", variableName = "contactsData")
public class Contacts {
@GqlField
private String email;
@GqlField
private String phoneNumber;
// getters and setters
...
}
Contacts contacts = new Contacts().setEmail("test@domain.com").setPhoneNumber("3751945");
String variableName = "id";
GqlArgument<Contacts> variable = GqlArgument.of("contacts", contacts);
String query = GqlRequestBodyGenerator.mutation("updateContacts")
.arguments(VariableGenerationStrategy.annotatedArgumentValueType(), variable)
.selectionSet(Contacts.class).generate();
Or you can provide your own variables generation strategy that implements VariablePickingStrategy interface.
If query or mutation has big arguments number it may be easier to create object to keep them all at one place and manage together. In that case you can use GqlDelegateArgument to simply pass such object as argument:
Contacts contacts = new Contacts().setEmail("test@domain.com").setPhoneNumber("3751945");
GqlArgument<Contacts> delegateArgument = GqlDelegateArgument.of(contacts);
String query = GqlRequestBodyGenerator.mutation("updateContacts")
.arguments(delegateArgument).selectionSet(Contacts.class).generate();
If you want to generate variables for delegated values you need to pass flag to GqlDelegateArgument and add GqlVariableType annotations to fields that should be passed as variables:
public class Contacts {
@GqlField
@GqlVariableType
private String email;
@GqlField
@GqlVariableType
private String phoneNumber;
// getters and setters
...
}
Contacts contacts = new Contacts().setEmail("test@domain.com").setPhoneNumber("3751945");
GqlArgument<Contacts> delegateArgument = GqlDelegateArgument.of(contacts, true);
String query = GqlRequestBodyGenerator.mutation("updateContacts")
.arguments(delegateArgument).selectionSet(Contacts.class).generate();
By default, only input argument value is treated as complex input objects according to GraphQL specification. However, you can use other model arguments strategies to override this behavior:
- treat only input argument as potential complex input object
String query = GqlRequestBodyGenerator.mutation("newUser")
.arguments(ModelArgumentGenerationStrategy.onlyInputArgument(), inputArgument)
.selectionSet(User.class).generate();
- treat any argument as potential complex input object
String query = GqlRequestBodyGenerator.mutation("newUser")
.arguments(ModelArgumentGenerationStrategy.anyArgument(), inputArgument)
.selectionSet(User.class).generate();
You can also provide your own mutation arguments strategy that implements ModelArgumentStrategy interface:
ModelArgumentStrategy modelArgumentStrategy = argument -> argument.getName().contains("Model");
String query = GqlRequestBodyGenerator.mutation("newUser")
.arguments(modelArgumentStrategy, inputArgument).selectionSet(User.class).generate();
When generating operation with variables resulted new operation will be anonymous by default. You can set
operation alias for convenience using operationAlias
method:
String variableName = "id";
String operationAlias = "id";
GqlParameterValue<Integer> variable = GqlVariableArgument.of(variableName, variableName, 1, true);
String query = GqlRequestBodyGenerator.query("getProfile")
.operationAlias("getProfileParameterized")
.arguments(VariableGenerationStrategy.byArgumentType(), variable)
.selectionSet(User.class).generate();
This project is licensed under the MIT License, you can read the full text here.