Java Client

Type-safe. No generics. No casting. Works with any JSON library.

Three generated interfaces

Instead of hardcoding a dependency on Jackson, Gson, or OkHttp, GraphLink generates three @FunctionalInterface types that you implement with lambdas. This means you choose your JSON library at construction time, and the generated code imports nothing external.

generated/interfaces/GraphLinkClientAdapter.java Java
// Executes an HTTP request.
// Receives the full JSON body as a String, returns the JSON response body as a String.
@FunctionalInterface
public interface GraphLinkClientAdapter {
    String execute(String payload);
}
generated/interfaces/GraphLinkJsonEncoder.java & GraphLinkJsonDecoder.java Java
// Serializes a Map/Object to a JSON string
@FunctionalInterface
public interface GraphLinkJsonEncoder {
    String encode(Object json);
}

// Deserializes a JSON string to Map<String, Object>
@FunctionalInterface
public interface GraphLinkJsonDecoder {
    Map<String, Object> decode(String json);
}

All three are @FunctionalInterface, so they can be assigned directly from lambdas. There is no base class to extend and no annotation processor to run.

Setting up with Jackson

Jackson is the most common choice. Here is the complete wiring from start to finish:

Main.java — Jackson setup Java
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

ObjectMapper mapper = new ObjectMapper();
HttpClient http = HttpClient.newHttpClient();

// Encode: Object (Map) -> JSON string
GraphLinkJsonEncoder encoder = obj -> mapper.writeValueAsString(obj);

// Decode: JSON string -> Map<String, Object>
GraphLinkJsonDecoder decoder = json -> mapper.readValue(json, Map.class);

// HTTP adapter: JSON string -> JSON string
GraphLinkClientAdapter adapter = payload -> {
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("http://localhost:8080/graphql"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(payload))
        .build();
    HttpResponse<String> response =
        http.send(request, HttpResponse.BodyHandlers.ofString());
    return response.body();
};

Any JSON library works
Replace ObjectMapper with Gson, Moshi, or org.json. The interfaces accept a String and return a String or Map — nothing is library-specific. The encoder/decoder lambdas are the only place where your JSON library appears.

Initializing the client

Creating the GraphLinkClient Java
import com.example.generated.client.GraphLinkClient;

// Parameters:
//   1. adapter  — the HTTP transport function
//   2. encoder  — JSON serializer
//   3. decoder  — JSON deserializer
//   4. store    — cache store (null = use InMemoryGraphLinkCacheStore)
GraphLinkClient client = new GraphLinkClient(adapter, encoder, decoder, null);

Pass a custom GraphLinkCacheStore as the fourth argument if you need a persistent cache (e.g. Redis-backed) or a custom eviction policy. 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:

Fetching a vehicle — GraphLink style Java
// 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:

The same query — typical other client Java
// 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:

getPerson — nullable result Java
// 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:

Adding a vehicle Java
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:

generated/inputs/AddVehicleInput.java Java
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:

List query Java
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:

GraphQL HTTP response JSON JSON
{
  "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.

Subscription support in Java
The Java client also generates subscription support via client.subscriptions. Subscriptions use a GraphLinkGraphLinkWebSocketAdapter (generated interface). Provide your own WebSocket adapter implementation when constructing the client for subscription support.