Java Client
Type-safe. No generics. No casting. Works with any JSON library.
Generated adapters
GraphLink generates concrete adapter classes into your client/ folder — no external GraphLink runtime, no boilerplate. Which adapters are generated is controlled by two options in config.json:
{
"clientConfig": {
"java": {
"packageName": "com.example.generated",
"wsAdapter": "java11",
"jsonCodec": "jackson"
}
}
}
wsAdapter — controls which WebSocket adapter is generated:
"java11"(default) — generatesDefaultGraphLinkWebSocketAdapterusing Java 11's built-injava.net.http.WebSocket. Zero external dependencies. Supports exponential-backoff auto-reconnect, an optionalSupplier<Map<String,String>>for auth headers, and automatically forwards those headers as theconnection_initpayload so your server can authenticate the WebSocket handshake."okhttp"— generates the same interface implemented with OkHttp's WebSocket client instead. Choose this if OkHttp is already in your dependency tree."none"— no WebSocket adapter is generated. Use this if you do not need subscriptions.
jsonCodec — controls which JSON codec is generated:
"jackson"(default) — generatesJacksonGraphLinkJsonCodecimplementing bothGraphLinkJsonEncoderandGraphLinkJsonDecoder."gson"— generatesGsonGraphLinkJsonCodecinstead."none"— no codec class is generated; supply your own lambdas.
Additionally, DefaultGraphLinkClientAdapter is always generated (when wsAdapter is not "none"). It uses Java 11's HttpClient for HTTP requests and accepts the same optional headers provider as the WebSocket adapter.
Initializing the client
The generated GraphLinkClient ships with several constructors that progressively add control. Start with the simplest one that fits your needs:
import com.example.generated.client.GraphLinkClient;
// Simplest setup — Jackson + Java 11 HttpClient + auto-derives wsUrl.
// wsUrl is derived by replacing "http" with "ws" in the provided URL.
GraphLinkClient client = new GraphLinkClient("http://localhost:8080/graphql");
// Dynamic auth headers on every request — token is fetched fresh each time.
Supplier<Map<String, String>> headers = () -> Map.of(
"Authorization", "Bearer " + tokenService.getToken()
);
// wsUrl auto-derived; both HTTP and WebSocket adapters receive the headers.
GraphLinkClient client = new GraphLinkClient(
"http://localhost:8080/graphql",
headers,
new JacksonGraphLinkJsonCodec(),
new JacksonGraphLinkJsonCodec()
);
// Use a different JSON library — e.g. Gson.
GraphLinkJsonEncoder encoder = obj -> gson.toJson(obj);
GraphLinkJsonDecoder decoder = json -> gson.fromJson(json, Map.class);
// Explicit wsUrl + custom codec.
GraphLinkClient client = new GraphLinkClient(
"http://localhost:8080/graphql",
"ws://localhost:8080/graphql",
encoder,
decoder
);
// Full manual wiring — bring your own adapter, encoder, decoder, and cache store.
GraphLinkClientAdapter adapter = payload -> { /* custom HTTP logic */ };
GraphLinkJsonEncoder encoder = obj -> mapper.writeValueAsString(obj);
GraphLinkJsonDecoder decoder = json -> mapper.readValue(json, Map.class);
GraphLinkWebSocketAdapter wsAdapter = new DefaultGraphLinkWebSocketAdapter(
"ws://localhost:8080/graphql"
);
GraphLinkClient client = new GraphLinkClient(
adapter, encoder, decoder, myCacheStore, wsAdapter
);
Any JSON library works
The three generated interfaces (GraphLinkClientAdapter, GraphLinkJsonEncoder, GraphLinkJsonDecoder) are all @FunctionalInterface — assign them from lambdas using Gson, Moshi, or any other library. Jackson is only the default for the convenience constructors.
Pass a custom GraphLinkCacheStore in the full constructor if you need a persistent or shared cache (e.g. Redis-backed). See the Caching page for details.
Queries — no generics
This is the core difference from every other Java GraphQL client. There are no TypeReference anonymous classes, no unchecked casts, no raw Map navigation. Each query method has a concrete return type:
// Clean, typed, no generics
GetVehicleResponse res = client.queries.getVehicle("42");
System.out.println(res.getGetVehicle().getBrand()); // Toyota
System.out.println(res.getGetVehicle().getYear()); // 2023
System.out.println(res.getGetVehicle().getFuelType()); // GASOLINE
Compare this to the boilerplate required by most other clients:
// What you're forced to write with most Java GraphQL clients
GraphQLResponse<Map<String, Object>> response =
client.query(new SimpleGraphQLRequest<>(
"query getVehicle($id: ID!) { getVehicle(id: $id) { id brand model year fuelType } }",
Map.of("id", "42"),
new TypeReference<GraphQLResponse<Map<String, Object>>>() {}
));
@SuppressWarnings("unchecked")
Map<String, Object> vehicleMap =
(Map<String, Object>) response.getData().get("getVehicle");
String brand = (String) vehicleMap.get("brand");
Integer year = ((Number) vehicleMap.get("year")).intValue();
Nullable queries
When the schema declares a nullable return type (no !), the corresponding getter on the response class returns a nullable type. In Java, this means it can be null:
// Schema: getPerson(id: ID!): Person <-- nullable return
GetPersonResponse res = client.queries.getPerson("99");
Person p = res.getGetPerson(); // can be null — check before use
if (p != null) {
System.out.println(p.getName());
System.out.println(p.getEmail());
} else {
System.out.println("Person not found");
}
Mutations — builder pattern
All input types are generated with an inner Builder class. Required fields (non-nullable in the schema) are validated with Objects.requireNonNull when build() is called:
import com.example.generated.inputs.AddVehicleInput;
import com.example.generated.enums.FuelType;
AddVehicleResponse added = client.mutations.addVehicle(
AddVehicleInput.builder()
.brand("Toyota")
.model("Camry")
.year(2023)
.fuelType(FuelType.GASOLINE)
// ownerId is nullable — omit for null
.build()
);
System.out.println(added.getAddVehicle().getId()); // server-assigned ID
System.out.println(added.getAddVehicle().getBrand()); // Toyota
The generated AddVehicleInput class:
public class AddVehicleInput {
private final String brand; private final String model;
private final Integer year; private final FuelType fuelType; private final String ownerId;
public AddVehicleInput(String brand, String model, Integer year, FuelType fuelType, String ownerId) {
Objects.requireNonNull(brand); Objects.requireNonNull(model);
Objects.requireNonNull(year); Objects.requireNonNull(fuelType);
this.brand = brand; this.model = model; this.year = year;
this.fuelType = fuelType; this.ownerId = ownerId;
}
public static Builder builder() { return new Builder(); }
public static class Builder {
private String brand; private String model; private Integer year;
private FuelType fuelType; private String ownerId;
public Builder brand(String brand) { this.brand = brand; return this; }
public Builder model(String model) { this.model = model; return this; }
public Builder year(Integer year) { this.year = year; return this; }
public Builder fuelType(FuelType fuelType) { this.fuelType = fuelType; return this; }
public Builder ownerId(String ownerId) { this.ownerId = ownerId; return this; }
public AddVehicleInput build() { return new AddVehicleInput(brand, model, year, fuelType, ownerId); }
}
}
Lists
List queries return a typed List<T> — no casting required:
ListVehiclesResponse res = client.queries.listVehicles();
List<Vehicle> vehicles = res.getListVehicles(); // List<Vehicle> — no raw types
for (Vehicle v : vehicles) {
System.out.printf("%s %s (%d) — %s%n",
v.getBrand(), v.getModel(), v.getYear(), v.getFuelType());
}
// Or with streams
vehicles.stream()
.filter(v -> v.getFuelType() == FuelType.ELECTRIC)
.map(Vehicle::getBrand)
.forEach(System.out::println);
The response wrapper pattern
Every query, mutation, and subscription operation generates a dedicated response class named {OperationName}Response. For example, getVehicle generates GetVehicleResponse.
This pattern mirrors the GraphQL JSON response structure, which always wraps results in a data field:
{
"data": {
"getVehicle": {
"id": "42",
"brand": "Toyota",
"model": "Camry",
"year": 2023,
"fuelType": "GASOLINE",
"ownerId": null
}
}
}
The generated GetVehicleResponse.fromJson() navigates into the data object and deserializes getVehicle as a Vehicle. From your code's perspective, you simply call res.getGetVehicle() — the JSON unwrapping is invisible.
Notice the double "get" in getGetVehicle() — the first is the Java getter prefix, the second is the operation name. This is consistent and predictable: the method name is always get + the operation name with a capital first letter.
Subscriptions
Subscriptions are available via client.subscriptions and use the GraphLinkWebSocketAdapter interface. The generated DefaultGraphLinkWebSocketAdapter implements this interface out of the box — no extra setup is needed as long as wsAdapter is not "none" in your config.
// client already holds a DefaultGraphLinkWebSocketAdapter — just subscribe
client.subscriptions.onCarCreated(event -> {
OnCarCreatedResponse res = event;
System.out.println("New car: " + res.getOnCarCreated().getMake());
});
The generated WebSocket adapter (Java 11 or OkHttp) handles the graphql-ws subprotocol automatically — connection init, ping/pong, and exponential-backoff reconnect on disconnect. If a headersProvider is supplied, the headers are forwarded in the connection_init payload so the server can authenticate the WebSocket session.
Deriving the WebSocket URL
The convenience constructors that take only an HTTP URL automatically derive the WebSocket URL by replacing http with ws (and https with wss). Pass an explicit wsUrl if your WebSocket endpoint differs from the HTTP endpoint.