MicroProfile GraphQL 1.0 has been focused on the server-side enabling to develop and expose GraphQL endpoints. The purpose of this specification is to define a so-called "dynamic" client API.
"Dynamic" means close to the GraphQL specification semantic and structure. We can compare it with what JAX-RS client API is for REST.
-
Provides full control over the MicroProfile GraphQL capabilities: operations, arguments, variables, scalars, input object, interface, partial results, errors…
-
Consistency with MicroProfile GraphQL server annotations (DateFormat, NumberFormat …) and capabilities
-
Consistency with MicroProfile:
-
No dependency outside MicroProfile core
-
Configuration exclusively based on MicroProfile Config
-
Support of JSON-B format directives
-
-
Transport layer support: the GraphQL specification is transport agnostic. We propose to stay aligned with this approach, leaving implementations free to use any client network library (JAX-RS, Apache HTTPClient …). However to make things more concrete, some examples using JAX-RS and HTTP are provided below.
A first version of the client API is planned with MicroProfile GraphQL 1.1.
For this first step, we propose to focus on the following core features.
bold: supported components.
italic bold: partially supported components.
blank: not supported components.
-
Document
-
Operation definition
-
Operation type
-
Query
-
Mutation
-
Subscription
-
-
Name
-
Variable definitions
-
Type
-
Default value
-
Directives
-
-
Directives
-
Selection set
-
Field
-
Alias
-
Name
-
Arguments
-
Variable
-
Int value
-
Float value
-
String value
-
Boolean value
-
Null value
-
Enum value
-
List value
-
Object value
-
-
Directives
-
-
Fragment spread
-
Inline fragment
-
-
-
Fragment definition
-
TypeSystem definition
-
TypeSystem extension
-
To be studied for next releases:
-
GraphQL subscription support
The usual workflow of the API is illustrated with the following snippet:
// Building of the graphql document.
Document myDocument = document(
operation(Operation.Type.QUERY,
field("people",
field("id"),
field("name")
)));
// Serialization of the document into a string, ready to be sent.
String graphqlRequest = myDocument.toString();
Static factory methods over constructors
In order to make the writing of request in Java as close as possible to the original GraphQL’s philosophy, it has been decided to make static factory methods an integral part of the API.
Of course, constructors can still be used but at the cost of clarity and ease of use.
@SafeVarargs
public static Document document(Operation... operations);
public static Document document(List<Operation> operations);
@SafeVarargs
public static List<Operation> operations(Operation... operations);
@SafeVarargs
public static Operation operation(Field... fields);
public static Operation operation(List<Field> fields);
@SafeVarargs
public static Operation operation(Type type, Field... fields);
public static Operation operation(Type type, List<Field> fields);
@SafeVarargs
public static Operation operation(String name, Field... fields);
public static Operation operation(String name, List<Field> fields);
@SafeVarargs
public static Operation operation(Type type, String name, Field... fields);
public static Operation operation(Type type, String name, List<Field> fields);
When omitted,
-
type parameter will default to QUERY
-
name parameter will default to an empty string
@SafeVarargs
public static List<Field> fields(Field... fields);
public static Field field(String name);
@SafeVarargs
public static Field field(String name, Field... fields);
public static Field field(String name, List<Field> fields);
@SafeVarargs
public static Field field(String name, Argument... args);
@SafeVarargs
public static Field field(String name, List<Argument> args, Field... fields);
public static Field field(String name, List<Argument> args, List<Field> fields);
Due to Java’s type erasure at compile-time, it is not possible to have both:
public static Field field(String name, List<Field> fields);
and
public static Field field(String name, List<Argument> args);
So, it has been decided to only retain:
public static Field field(String name, List<Field> fields);
When omitted, args and fields parameters will default to an empty list.
@SafeVarargs
public static List<Argument> args(Argument... args);
public static Argument arg(String name, Object value);
@SafeVarargs
public static InputObject object(InputObjectField... inputObjectFields);
public static InputObject object(List<InputObjectField> inputObjectFields);
public static InputObjectField prop(String name, Object value);
The keyword prop (as in an object’s property) has been chosen instead of field to avoid confusion with the notion of field of a selection set.
Once a GraphQL document has been prepared, it can be run against a server. This specification proposes two abstractions for that:
-
GraphQLRequest: prepare a request execution including the request and optional variables
-
GraphQLResponse: a holder for a GraphQL response including optional errors and data.
A GraphQLClientBuilder class is defined to bootstrap a client implementation. This can be done using the Service Loader approach.
public interface GraphQLRequest {
GraphQLRequest addVariable(String name, Object value);
GraphQLRequest resetVariables();
String toJson();
}
A GraphQLRequest object is initialised from the builder with a GraphQL request obtained from a Document:
GraphQLRequest graphQLRequest = new graphQLClientBuilder.newRequest(document.toString());
Optional GraphQL variables can be provided in a fluent manner:
graphQLRequest
.addVariable("surname", "James")
.addVariable("personId", 1);
In order to make it reuseable for other executions, variables can also be reset:
graphQLRequest
.resetVariables()
.addVariable("surname", "Roux")
.addVariable("personId", 2);
With this approach, a GraphQLRequest object is immutable regarding the GraphQL request to run and mutable regarding the variables. It is the responsibility of the caller to assign the consistency between the request and the variables.
Once initialized with a request and optional variables, a GraphQLrequest object can be sent to a GraphQL server. As mentioned in the "non-goal" paragraph, this specification is deliberatly transport agnostic. It is the responsibility of the implementation to propose a transport layer.
For instance:
-
JAX-RS in a Jakarta EE or MicroProfile container
-
raw HTTP using a library such as Apache HTTP client.
To make things more concrete, we propose some examples using JAX-RS.
Suppose we a have an initialized GraphQLRequest. It can be a mutation or a query. We can send it and get the response in the following way;
Client client = clientBuilder.build();
Response response = client
.target("http://localhost:8080/graphql")
.request(MediaType.APPLICATION_JSON)
.post(json(graphQLRequest));
A registered JAX-RS MessageBodyWriter is needed to automatically turn a GraphQLRequest object into a JSON structure. This is the responsibility of the implementation to provide it.
In the previous example, a generic JAX-RS Response is returned. The GraphQLResponse (described below) can then be read as an entity:
GraphQLResponse graphQLResponse=response
.readEntity(GraphQLResponse.class);
Alternatively, we can get a GraphQLResponse directly as a typed entity:
GraphQLResponse graphQLResponse = client
.target("http://localhost:8080/graphql")
.request(MediaType.APPLICATION_JSON)
.post(json(graphQLRequest), GraphQLResponse.class);
A registered JAX-RS MessageBodyReader is needed to turn a JSON structure into a GraphQLResponse object. This is the responsibility of the implementation to provide it.
Using JAX-RS, we can even run a request in a reactive way:
CompletionStage<GraphQLResponse> csr = client
.target("http://localhost:8080/graphql")
.request()
.rx()
.post(json(graphQLRequest),GraphQLResponse.class);
// Do some other stuff here...
csr.thenAccept(// Async processing here });
Let’s see how to use a HTTP transport layer with Apache HttpClient:
// Prepare the HTTP POST
URI endpoint = new URI("http://localhost:8080/graphql");
HttpPost httpPost = new HttpPost(new URI(endpoint));
StringEntity stringEntity = new StringEntity(jsonRequest.toJson(), ContentType.APPLICATION_JSON);
httpPost.setEntity(stringEntity);
// Execute the POST
CloseableHttpClient httpClient = HttpClients.createDefault());
CloseableHttpResponse httpResponse=httpClient.execute(httpPost);
// Read the response
InputStream contentStream = serverResponse.getEntity().getContent();
For the sake of simplicity, this code does not take into account configuration, exception and resource management and omits the details of data conversion.
In the previous examples, we have seen how to get a GraphQLResponse from a server.
GraphQLResponse is a holder both for data and errors.
public interface GraphQLResponse {
JsonObject getData();
List<GraphQLError> getErrors();
<T> List<T> getList(Class<T> dataType, String rootField);
<T> T getObject(Class<T> dataType, String rootField);
boolean hasData();
boolean hasError();
public static interface GraphQLError {
String getMessage();
List<Map<String, Integer>> getLocations();
Object[] getPath();
Map<String, Object> getExtensions();
}
}
We can check if there is any error and access each of them:
if ( graphQLResponse.hasError() ) {
log.warn("GraphQL error:");
graphQLResponse.getErrors().forEach( e -> log.warning(e.toString()) );
}
The getErrors() method returns a list of GraphQLError objects. In accordance with the specification, a GraphQLError is made of:
-
a message
-
a list of locations
-
an array of path
-
a map of extensions
It is the responsibility of the client to decide how to deal with GraphQL errors.
The hasData method enables to check if there is any data:
if ( graphQLResponse.hasData() )
log.info("Data inside");
Data can be obtained in 2 ways:
-
as a generic JsonObject: using the getData method, it is the responsibility of the caller to turn this JsonObject into application objects
-
as an application object (or a list of them): using the getObject (or getList) method. In that case, it is necessary to provide the expected data rootfield to be retrieved.
For instance, with a UserProfile application class:
// Get the data as a generic JsonObject
JsonObject data = graphQLResponse.getData();
// Turn it into a UserProfile object
JsonObject myData = data.getJsonObject("profile");
Jsonb jsonb = JsonbBuilder.create();
UserProfile userProfile = jsonb.fromJson(myData.toString(), Profile.class);
// OR
// Directly get a UserProfile object from graphqlReponse
UserProfile userProfile = graphQLResponse.getObject(Profile.class, "profile");
In the same way, the getList method enables to get a list of objects:
// Get a list of Person from a graphQLResponse
List<Person> people = graphQLResponse.getList(Person.class, "people");