REST & JSON Libraries

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against GWT 2.13.1 (2026-06-19), gwt-maven-plugin 1.3.0, Elemental2 1.3.2, a JDK 21 toolchain with -sourceLevel 17, the Jakarta Servlet 6 stack of the modular-webapp archetype and Spring Boot 4.1.1 (Spring Framework 7.0.9, Spring Security 7.1.1, Tomcat 11.0.24), and was checked on 2026-10-09. Every output block was produced with the Bookshelf scenario of this section: a probe module compiled to JavaScript for each library, a Spring Boot application serving the REST API and the compiled files, and headless Chrome 155 loading the page and reporting the log. It covers client code (the REST and JSON code that is compiled to JavaScript), shared code (the DTO classes the client libraries need), server code (the Spring Boot REST backend and a proxy to a third-party service) and build settings (the Maven coordinates, annotation processors and module inherits each library needs).

GWT-RPC is the way GWT calls a Java servlet with Java objects. Many applications talk to something else: a REST API that other clients also use, a service written in another language, or a public JSON API. For that, the Developer Guide says the application is free to integrate "other RPC mechanisms, such as JSON using the GWT supplied RequestBuilder class, JSNI methods or a third party library". This page is about the third-party and browser-native options, and about the server side of the same conversation.

flowchart TD start["Client needs JSON over HTTP"] --> q1{"Do other clients need the same API?"} q1 -->|"no, GWT only"| rpc["GWT-RPC or RequestFactory
typed Java objects, no JSON"] q1 -->|"yes"| q2{"How much typing do you want?"} q2 -->|"a few calls"| fetch["Elemental2 fetch
native JSON types, no extra library"] q2 -->|"many endpoints"| q3{"Which JSON mapping?"} q3 -->|"annotation processor"| domino["domino-rest + domino-jackson
actively released"] q3 -->|"generator based, legacy"| old["RestyGWT or gwt-jackson
released 2020 and 2018"] q3 -->|"RxJava Single and Observable"| auto["AutoREST
last release 2023"]

The diagram is a summary of the sections below, not a rule: each branch is described with its Maven coordinates, a probe that ran, and the status I could verify. The decision table that compares the communication styles as a whole, GWT-RPC, RequestBuilder, JSONP and RequestFactory included, is on the Client–Server Communication page; the GWT-RPC and RequestBuilder, JSON & XML pages describe those styles in detail.

The libraries and their status

All figures below were read on 2026-10-09 from Maven Central directory listings, the GitHub API and each project README. They describe release dates and commit dates, which is what can be verified; none of the projects publishes an end-of-life notice that I could find, so "status" means what the dates show.

Table 1. Status of the REST and JSON options for GWT (checked 2026-10-09)
Option Latest release on Maven Central Last commit on the default branch Compiles and runs on GWT 2.13.1, JDK 21 Notes

Elemental2 fetch

com.google.elemental2:elemental2-dom 1.3.2 (2025-09-15); 1.4.0-RC1 exists (2026-07-07)

repository google/elemental2 pushed 2026-09-27

yes (probe below)

Browser API, no JSON mapping; JSON shapes are JsInterop native types. Not part of the GWT BOM, so versions are yours to pin

domino-rest

org.dominokit:domino-rest-client 2.0.2 (2026-09-24); 2.0.0 on 2025-12-30, 2.0.1 on 2026-07-29

2026-09-24 (release/2.0.2 merged)

yes (probe below)

Annotation-processor generated clients from Jakarta REST interfaces. Its README says the generated client works on GWT 2, GWT 3 / J2CL and the JVM; I verified only GWT 2.13.1

domino-jackson

org.dominokit:domino-jackson 1.0.6 (2026-07-27)

2026-07-27 (release/1.0.6 merged)

yes (through domino-rest)

Annotation-processor JSON mapper; its README says it is based on gwt-jackson. The README release badge still shows 1.0.0-RC3, which is out of date

RestyGWT

org.fusesource.restygwt:restygwt 2.2.7 (2020-05-17)

2020-06-30 on master

yes, after adding the JAXB API jar (see pitfalls)

Generator-based, javax.ws.rs annotations. The maven-metadata.xml on Central still says 1.4 is the latest; the directory listing has the 2.x releases. The project home page shows 2.2.0 as the latest stable release

AutoREST

com.intendia.gwt.autorest:autorest-gwt 0.11 (2023-11-24)

2023-11-24

yes (probe below)

Annotation-processor generated models returning RxJava 2 types; built against GWT 2.10.0 and Elemental2 1.1.0 according to its POM. The exception base class is marked experimental in its sources

gwt-jackson

com.github.nmorel.gwtjackson:gwt-jackson 0.15.4 (2018-09-16)

2021-02-21 (README edit)

yes (probe below)

Generator-based JSON mapper using Jackson 2 annotations; no release since 2018

Reading the table:

  • Pick by the maintenance signal you need. domino-rest and domino-jackson published releases within the last three months of the check date; Elemental2 published a release candidate (1.4.0-RC1) in July 2026, while its latest stable release, 1.3.2, is thirteen months old. RestyGWT, AutoREST and gwt-jackson did not publish a release in the last two years, but all three still compiled and ran against GWT 2.13.1 in the probes below, which is not a promise that they will with the next GWT release.

  • A library that works today still needs an owner. If you adopt one of the three quiet projects, plan to fork or replace it: the annotation processors and generators of those projects run inside the GWT compiler or javac, and a future JDK or GWT release can break them without anyone to fix them.

  • GWT itself documents none of them. The Developer Guide names RequestBuilder, JSNI and "a third party library"; it never names RestyGWT, AutoREST, gwt-jackson or DominoKit. The Ecosystem & Libraries page lists the wider library landscape.

The probes in this page were compiled with the GWT compiler, either through gwt-maven-plugin 1.3.0 (gwt-app packaging, which I used for domino-rest and RestyGWT) or through the com.google.gwt.dev.Compiler class with an explicit class path (used for all five), and loaded in headless Chrome. The sizes of the compiled *.cache.js files, one permutation each, are given for orientation only, because the probes use different features:

Table 2. Compiled size of one permutation of each probe
Probe Bytes Bytes gzipped What it contains

Elemental2 fetch wrapper

31,179

10,626

the full CRUD probe of this page, GWT Cookies, AsyncCallback adapter

domino-rest + domino-jackson

134,473

43,117

list, get and create calls, two mappers, one request interceptor, org.gwtproject.* runtime modules

RestyGWT

52,726

18,039

list, get and create calls, one dispatcher

gwt-jackson with RequestBuilder

75,392

25,769

two mappers and one RequestBuilder call

AutoREST

51,398

17,663

list and get calls with RxJava Single

The Bookshelf REST API (Spring Boot)

Every client probe below talks to the same server: a Spring Boot application with a Bookshelf controller. This is the server half of the conversation; the Spring side is documented in depth in REST APIs with Spring MVC and WebFlux (controllers, Bean Validation, ProblemDetail, CORS), and the authentication side in Spring Security. The earlier pages of this section used a read-only servlet at /api/books; the controller below serves the same URL with the full set of verbs.

Table 3. The contract the clients use
Verb Path Success Failure

GET

/api/books?start=0&length=10

200, {"total":4,"books":[...]}

none expected; length is capped at 100 by the server

GET

/api/books/{id}

200, one book

404 application/problem+json with detail No book 99

POST

/api/books

201 with a Location header and the saved book

400 for a body that fails validation; 401 or 403 without authentication or without the CSRF header

PUT

/api/books/{id}

200, the saved book

404, 400, 401 or 403

DELETE

/api/books/{id}

204, no body

404, 401 or 403

The payload is a DTO, never a JPA entity: the record fixes the JSON shape that the browser depends on, and a change to the database mapping cannot leak into the contract by accident.

public record BookDto(
    Long id,
    @NotBlank @Size(max = 120) String title,
    @NotBlank @Size(max = 80) String author,
    @Min(1450) @Max(2100) int year,
    @Min(0) int copies) {
}

public record BookPageDto(int total, List<BookDto> books) {
}

@RestController
@RequestMapping("/api/books")
public class BookController {
  private final BookStore store;

  public BookController(BookStore store) {
    this.store = store;
  }

  @GetMapping
  public BookPageDto list(@RequestParam(defaultValue = "0") int start,
                          @RequestParam(defaultValue = "10") int length) {
    return new BookPageDto(store.total(), store.slice(start, Math.min(length, 100)));
  }

  @GetMapping("/{id}")
  public BookDto get(@PathVariable long id) {
    return store.find(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No book " + id));
  }

  @PostMapping
  public ResponseEntity<BookDto> create(@Valid @RequestBody BookDto body) {
    BookDto saved = store.add(body);
    return ResponseEntity.created(URI.create("/api/books/" + saved.id())).body(saved);
  }

  @PutMapping("/{id}")
  public BookDto update(@PathVariable long id, @Valid @RequestBody BookDto body) {
    return store.replace(id, body)
        .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No book " + id));
  }

  @DeleteMapping("/{id}")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void delete(@PathVariable long id) {
    if (!store.remove(id)) {
      throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No book " + id);
    }
  }
}

BookStore is an in-memory map in the probe, standing in for the repository of a real application. The configuration needs three lines, one of which turns Spring errors into RFC 9457 problem documents:

server.port=8934
spring.mvc.problemdetails.enabled=true
spring.security.user.name=librarian
spring.security.user.password=${BOOKSHELF_PASSWORD}
spring.security.user.roles=LIBRARIAN

The password comes from the environment (the probes were run with the placeholder value PASSWORD_PLACEHOLDER); never commit a real one, and use a user store or an identity provider in a real application.

Security: authorisation and CSRF

Reading is public, writing needs the LIBRARIAN role, and the browser proves it is the same application with a double-submit cookie:

@Configuration
public class SecurityConfig {

  @Bean
  SecurityFilterChain api(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.spa())
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(HttpMethod.GET, "/api/**", "/", "/*.html", "/restdemo/**").permitAll()
            .requestMatchers("/api/**").hasRole("LIBRARIAN")
            .anyRequest().denyAll())
        .httpBasic(Customizer.withDefaults());
    return http.build();
  }
}

csrf.spa() (present in Spring Security 7.1.1 and shown on the Spring Security CSRF page) configures a cookie repository that is not HttpOnly, so the page can read it: the server writes a cookie named XSRF-TOKEN and expects its value back in a request header named X-XSRF-TOKEN. httpBasic is used here only because it keeps the probe small; a real application uses a form login, OAuth 2.0 or OpenID Connect, as the Spring Security page explains. The authorisation rule is on the server: hiding a button in the GWT client does not protect anything, because the client code is public.

sequenceDiagram participant B as GWT client in the browser participant S as Spring Boot B->>S: GET /api/books (fetch, same origin) S-->>B: 200 JSON and Set-Cookie XSRF-TOKEN Note over B: Cookies.getCookie reads XSRF-TOKEN B->>S: POST /api/books with Cookie and header X-XSRF-TOKEN S-->>B: 201 Created, Location /api/books/5 B->>S: DELETE /api/books/1 without the header S-->>B: 403 Forbidden, empty body

The responses below were produced with curl against the running application. Cookie values are replaced by CSRF_TOKEN_PLACEHOLDER (the server generates a different UUID-shaped value for each response), and the cache-control and other security headers are left out.

curl -si "http://localhost:8934/api/books?start=0&length=2"
HTTP/1.1 200
Set-Cookie: XSRF-TOKEN=CSRF_TOKEN_PLACEHOLDER; Path=/
X-Content-Type-Options: nosniff
Content-Type: application/json

{"total":4,"books":[{"id":1,"title":"The Pragmatic Programmer","author":"Andrew Hunt","year":1999,"copies":3},{"id":2,"title":"Effective Java","author":"Joshua Bloch","year":2001,"copies":2}]}

A write needs the role, the cookie and the matching header:

curl -si -u librarian:PASSWORD_PLACEHOLDER -X POST http://localhost:8934/api/books \
  -H 'Content-Type: application/json' \
  -H 'Cookie: XSRF-TOKEN=CSRF_TOKEN_PLACEHOLDER' -H 'X-XSRF-TOKEN: CSRF_TOKEN_PLACEHOLDER' \
  -d '{"title":"Dune","author":"Frank Herbert","year":1965,"copies":2}'
HTTP/1.1 201
Location: /api/books/5
Content-Type: application/json

{"id":5,"title":"Dune","author":"Frank Herbert","year":1965,"copies":2}

Without the header, with the same credentials, the answer is not 403:

HTTP/1.1 401
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"
Content-Length: 0

CsrfFilter runs before BasicAuthenticationFilter, so the request is still anonymous when the token check fails, and the authentication entry point answers with 401 instead of the access-denied handler’s 403 (the Spring debug log said Invalid CSRF token found for http://localhost:8934/api/books/4). A client must therefore treat both 401 and 403 on a write as "not allowed or token missing" and fetch a fresh token with a GET before retrying once. When the same check runs for a request that needs no authentication, as in the headless-browser probe of the Elemental2 section below, the answer is 403 with an empty body.

A body that fails validation returns a problem document, but Spring Boot 4.1.1 does not list the offending fields in it by default:

HTTP/1.1 400
Content-Type: application/problem+json

{"detail":"Invalid request content.","instance":"/api/books","status":400,"title":"Bad Request"}

and an unknown id gives the 404 of the contract:

HTTP/1.1 404
Content-Type: application/problem+json

{"detail":"No book 99","instance":"/api/books/99","status":404,"title":"Not Found"}

The spring side of CORS is described in REST APIs: cross-origin requests and the concepts in What is CORS?. The probes avoid CORS entirely by serving the compiled GWT files and the API from the same origin, which is also what the Server Integration & Deployment page recommends: no Access-Control-Allow-Origin header, no preflight request, and a session cookie that never leaves the site.

Elemental2 fetch

Elemental2 is Google’s set of Java type definitions for the browser APIs, generated from the JavaScript externs (see DOM and Elemental2 for the types, DomGlobal, Promise and a first fetch example). For REST it gives you DomGlobal.fetch, Response, Headers, RequestInit, AbortSignal and Promise, and nothing else: the library has no JSON-to-object mapper, so the shape of a response is a JsInterop native type that costs no code. This is the smallest dependency set of all the options, and the right choice for a handful of calls.

<dependency>
  <groupId>com.google.elemental2</groupId>
  <artifactId>elemental2-dom</artifactId>
  <version>1.3.2</version>
</dependency>
<dependency>
  <groupId>com.google.elemental2</groupId>
  <artifactId>elemental2-core</artifactId>
  <version>1.3.2</version>
</dependency>
<dependency>
  <groupId>com.google.elemental2</groupId>
  <artifactId>elemental2-promise</artifactId>
  <version>1.3.2</version>
</dependency>
<module rename-to="restdemo">
  <inherits name="com.google.gwt.user.User"/>
  <inherits name="elemental2.dom.Dom"/>
  <inherits name="elemental2.core.Core"/>
  <inherits name="elemental2.promise.Promise"/>

  <source path="restdemo"/>
  <entry-point class="com.example.bookshelf.restdemo.RestDemo"/>
</module>

The data shapes

A native type with name = "Object" is "a plain JavaScript object with these properties"; no class is generated and no copying happens. The @JsOverlay factory is the only code, and it is inlined:

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class BookJson {
  public double id;
  public String title;
  public String author;
  public int year;
  public int copies;

  @JsOverlay
  public static BookJson create(String title, String author, int year, int copies) {
    BookJson book = new BookJson();
    book.title = title;
    book.author = author;
    book.year = year;
    book.copies = copies;
    return book;
  }
}

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class BookPageJson {
  public int total;
  public JsArray<BookJson> books;
}

The id is a double because a JavaScript number is a double: a long field in a native type is not a safe way to read a number from JSON, and ids above 2^53 lose precision whatever the Java type is. If the server uses 64-bit ids that large, send them as strings. JsInterop is described on the JsInterop page.

A small fetch wrapper

Hand-written calls repeat the same four decisions: the headers, the CSRF token, the deadline and what an error status means. Put them in one class:

public final class Rest {
  static final String XSRF_COOKIE = "XSRF-TOKEN";
  static final String XSRF_HEADER = "X-XSRF-TOKEN";
  static final double DEFAULT_TIMEOUT_MS = 5000;

  public static Promise<Response> send(String method, String url, Object body, double timeoutMs) {
    RequestInit init = RequestInit.create();
    init.setMethod(method);
    init.setCredentials("same-origin");
    init.setSignal(AbortSignal.timeout(timeoutMs));

    Headers headers = new Headers();
    headers.set("Accept", "application/json");
    if (body != null) {
      headers.set("Content-Type", "application/json");
      init.setBody(Global.JSON.stringify(body));
    }
    if (!isSafe(method)) {
      String token = Cookies.getCookie(XSRF_COOKIE);
      if (token != null) {
        headers.set(XSRF_HEADER, token);
      }
    }
    init.setHeaders(headers);

    return DomGlobal.fetch(url, init).then(res -> res.ok ? Promise.resolve(res) : failed(res));
  }

  public static <T> Promise<T> json(String method, String url, Object body) {
    return send(method, url, body, DEFAULT_TIMEOUT_MS)
        .then(res -> res.status == 204 ? Promise.resolve((Object) null) : res.json())
        .then(value -> Promise.resolve(Js.<T>uncheckedCast(value)));
  }

  static boolean isSafe(String method) {
    return "GET".equals(method) || "HEAD".equals(method) || "OPTIONS".equals(method);
  }

  private static Promise<Response> failed(Response res) {
    return res.text().then(text -> {
      String detail = res.status + " " + res.statusText;
      if (text != null && text.startsWith("{")) {
        JsPropertyMap<Object> problem = Js.asPropertyMap(Global.JSON.parse(text));
        Object d = problem.get("detail");
        if (d != null) {
          detail = res.status + " " + d;
        }
      }
      return Promise.reject(new HttpException(res.status, detail));
    });
  }
}

What each decision is, and why:

  • setSignal(AbortSignal.timeout(ms)). fetch has no timeout of its own. AbortSignal.timeout aborts the request after the delay and rejects the promise with a TimeoutError (reproduced below with a delay of 0). Without it a hung server leaves the screen waiting for ever.

  • setCredentials("same-origin"). It is the default, written out so the intent is reviewable: the session cookie goes to the page’s own origin and to no other.

  • Cookies.getCookie is the GWT class (com.google.gwt.user.client.Cookies); Elemental2 has no cookie API beyond the document.cookie string. The wrapper sends the token on every method except GET, HEAD and OPTIONS, the "safe" methods of RFC 9110.

  • An error status resolves the promise. fetch rejects only for network failures and aborts. The wrapper turns a response with ok == false into a rejection of its own type, HttpException, which carries the status and the detail of the problem document.

  • The Java exception is the rejection value. Promise.reject(new HttpException(...)) rejects with the Java object, so a later handler can test error instanceof HttpException. I did not test a throw inside a callback; the explicit reject is the form that was tested.

HttpException is a plain RuntimeException with an int status.

Typed calls

public final class BooksClient {
  private static final String BASE = "/api/books";

  public static Promise<BookPageJson> list(int start, int length) {
    return Rest.json("GET", BASE + "?start=" + start + "&length=" + length, null);
  }

  public static Promise<BookJson> get(long id) {
    return Rest.json("GET", BASE + "/" + id, null);
  }

  public static Promise<BookJson> create(BookJson draft) {
    return Rest.json("POST", BASE, draft);
  }

  public static Promise<BookJson> update(long id, BookJson book) {
    return Rest.json("PUT", BASE + "/" + id, book);
  }

  public static Promise<Object> delete(long id) {
    return Rest.json("DELETE", BASE + "/" + id, null);
  }
}

Path segments that come from users must be encoded with Global.encodeURIComponent (and query values with URLSearchParams, see the DOM & Elemental2 page); the ids above are numbers, so the strings need no encoding.

Running a whole conversation

The entry point chains the calls. The first lines show the shape; the other steps (update, delete, an invalid book, an unknown id, a write without a token, a timeout, an HTML page parsed as JSON, a cross-origin call) follow the same pattern:

BooksClient.list(0, 2)
    .then(page -> {
      log("list         : total=" + page.total + " returned=" + page.books.length);
      for (int i = 0; i < page.books.length; i++) {
        BookJson b = page.books.getAt(i);
        log("  book       : #" + (long) b.id + " " + b.title + " (" + b.year + ")");
      }
      return BooksClient.get(2);
    })
    .then(book -> {
      log("get(2)       : " + book.title + " by " + book.author);
      log("xsrf cookie  : " + (Cookies.getCookie(Rest.XSRF_COOKIE) != null ? "present" : "missing"));
      return BooksClient.create(BookJson.create("Dune", "Frank Herbert", 1965, 2));
    })
    .then(created -> {
      log("create       : id=" + (long) created.id + " " + created.title);
      created.copies = 5;
      return BooksClient.update((long) created.id, created);
    })
    // ... delete, invalid, unknown id, no token, timeout, callback, Promise.all
    .catch_(error -> {
      log("UNEXPECTED   : " + describe(error));
      return null;
    });

Compiled and loaded in headless Chrome 155 against the Spring Boot application, with authentication switched off and CSRF protection on (a harness profile; a headless browser cannot answer a Basic authentication prompt), the log was:

list         : total=4 returned=2
  book       : #1 The Pragmatic Programmer (1999)
  book       : #2 Effective Java (2001)
get(2)       : Effective Java by Joshua Bloch
xsrf cookie  : present
create       : id=7 Dune
update       : copies=5
delete       : 204, no body
invalid      : HttpException 400 Invalid request content.
get(99)      : HttpException 404 No book 99
no token     : HttpException 403
timeout 0 ms : TimeoutError: signal timed out
callback     : success Clean Code
Promise.all  : 2 responses
html as json : SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON
cross origin : TypeError: Failed to fetch

Things to notice:

  • The id of the created book depends on how many runs created one before; the server assigns it.

  • no token is a 403 with an empty status text (HTTP/1.1 from Tomcat sends no reason phrase), so the wrapper’s detail is just the status number.

  • html as json is what happens when the URL returns the host page instead of JSON, typically because a proxy or a route fallback answered; the error is the browser’s Response.json() failure, not a Java exception.

  • cross origin is a call to the same server through a different host name (127.0.0.1 from a page loaded from localhost). The browser blocked it, and logged Access to fetch at 'http://127.0.0.1:8934/api/books' from origin 'http://localhost:8934' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. The script only learns TypeError: Failed to fetch.

Adapting a Promise to an AsyncCallback

Presenters written for the GWT-RPC style (see the MVP page, Activities and Places) keep working if the client returns an AsyncCallback. An adapter is five lines:

public static <T> void call(Promise<T> promise, AsyncCallback<T> callback) {
  promise.then(
      value -> {
        callback.onSuccess(value);
        return null;
      },
      error -> {
        callback.onFailure(error instanceof Throwable ? (Throwable) error : new RuntimeException(String.valueOf(error)));
        return null;
      });
}

The two-argument then is deliberate: an exception thrown by onSuccess is not routed to onFailure, so a bug in the presenter is not reported as a network error. This is also the shape of the bridge to the RequestBuilder style (see RequestBuilder, JSON & XML): a RequestCallback becomes a Promise in the same way.

Mapping JSON without a library

There are three ways to turn a response into Java without a mapper, in order of cost:

Table 4. JSON mapping without a library
Way When Limits

Native JsInterop type (BookJson) read from Response.json()

the usual choice; zero code per property

no validation: a missing field is undefined; long and Date are not supported by JSON itself

JsPropertyMap (Js.asPropertyMap)

a payload whose shape you do not control, or a map of unknown keys

every access is a string key and an Object

JsonUtils and JSONParser (GWT’s own classes)

old code, and the JSONValue tree model

described on the RequestBuilder, JSON & XML page

A library mapper (next sections) gains real Java objects with getters, setters, collections and annotations, and loses the zero-code property: every DTO has a generated serializer and deserializer in the compiled output.

domino-rest and domino-jackson

DominoKit publishes two libraries that are designed together, and are the only ones in this page with releases in 2026. domino-jackson is a JSON mapper: it reads and writes Java objects as JSON, generated at compile time by an annotation processor. domino-rest generates a REST client from a Jakarta REST interface; its JSON handling is domino-jackson. The domino-rest README states that the same client works in the browser (GWT 2 or GWT 3 / J2CL) and on the JVM, and the domino-jackson README says its generated mappers avoid runtime reflection, which matters for GWT and J2CL. Both generate their code with annotation processors, which run in javac before the GWT compiler starts, instead of with GWT generators.

Build

<properties>
  <domino.rest.version>2.0.2</domino.rest.version>
</properties>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.gwtproject</groupId>
      <artifactId>gwt</artifactId>
      <version>2.13.1</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    <dependency>
      <groupId>com.google.elemental2</groupId>
      <artifactId>elemental2-dom</artifactId>
      <version>1.3.2</version>
    </dependency>
    <dependency>
      <groupId>com.google.elemental2</groupId>
      <artifactId>elemental2-core</artifactId>
      <version>1.3.2</version>
    </dependency>
    <dependency>
      <groupId>com.google.elemental2</groupId>
      <artifactId>elemental2-promise</artifactId>
      <version>1.3.2</version>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.gwtproject</groupId>
    <artifactId>gwt-user</artifactId>
    <scope>provided</scope>
  </dependency>
  <dependency>
    <groupId>org.gwtproject</groupId>
    <artifactId>gwt-dev</artifactId>
    <scope>provided</scope>
  </dependency>
  <dependency>
    <groupId>org.dominokit</groupId>
    <artifactId>domino-rest-client</artifactId>
    <version>${domino.rest.version}</version>
  </dependency>
  <dependency>
    <groupId>org.dominokit</groupId>
    <artifactId>domino-rest-processor</artifactId>
    <version>${domino.rest.version}</version>
    <scope>provided</scope>
  </dependency>
</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.15.0</version>
      <configuration>
        <release>17</release>
        <annotationProcessorPaths>
          <path>
            <groupId>org.dominokit</groupId>
            <artifactId>domino-rest-processor</artifactId>
            <version>${domino.rest.version}</version>
          </path>
        </annotationProcessorPaths>
      </configuration>
    </plugin>
    <plugin>
      <groupId>net.ltgt.gwt.maven</groupId>
      <artifactId>gwt-maven-plugin</artifactId>
      <version>1.3.0</version>
      <extensions>true</extensions>
      <configuration>
        <moduleName>com.example.bookshelf.DominoDemo</moduleName>
        <moduleShortName>dominodemo</moduleShortName>
        <sourceLevel>17</sourceLevel>
        <failOnError>true</failOnError>
      </configuration>
    </plugin>
  </plugins>
</build>

(The <packaging> of this POM is gwt-app, and the module file is in src/main/resources.)

The Elemental2 versions are pinned because domino-rest-client 2.0.2 brings in elemental2-dom 1.1.0 and elemental2-core 1.2.3 transitively (read from mvn dependency:build-classpath before the pin), several releases older than the 1.3.2 of the other pages. domino-jackson and the org.gwtproject.* runtime modules (gwt-core, gwt-event, gwt-timer, gwt-xhr, gwt-regexp, gwt-typedarrays) arrive transitively; the jakarta.ws.rs API is the 3.1.0 jar plus the domino-rest-jaxrs shim, which carries a GWT module for it.

<module rename-to="dominodemo">
  <inherits name="com.google.gwt.user.User"/>
  <inherits name="org.dominokit.rest.Rest"/>
  <inherits name="org.dominokit.jackson.Jackson"/>

  <source path="domino"/>
  <entry-point class="com.example.bookshelf.domino.DominoDemo"/>
</module>

DTOs, the service and the call

A DTO is a plain class annotated @JSONMapper; a service is an interface annotated @RequestFactory with Jakarta REST annotations:

@JSONMapper
public class BookDto {
  public Long id;
  public String title;
  public String author;
  public int year;
  public int copies;
}

@JSONMapper
public class BookPageDto {
  public int total;
  public List<BookDto> books;
}

@RequestFactory(serviceRoot = "/api")
public interface BooksService {

  @GET
  @Path("books")
  BookPageDto list(@QueryParam("start") int start, @QueryParam("length") int length);

  @GET
  @Path("books/{id}")
  BookDto get(@PathParam("id") long id);

  @POST
  @Path("books")
  BookDto create(@RequestBody BookDto draft);
}

The annotation types come from jakarta.ws.rs (GET, POST, Path, PathParam, QueryParam) and from org.dominokit.rest.shared.request.service.annotations (RequestFactory, RequestBody), and JSONMapper from org.dominokit.jackson.annotation. javac runs the processor during mvn compile and writes, for this probe, seven files to target/generated-sources/annotations: BooksServiceFactory, and for each DTO a BookDto_MapperImpl, a BookDtoBeanJsonSerializerImpl and a BookDtoBeanJsonDeserializerImpl (the same three for the page). The generated factory has one method per interface method, returning a ServerRequest<RequestType, ResponseType>:

public ServerRequest<Void, BookPageDto> list(int start, int length) {
  BooksService_list instance = new BooksService_list();
  ParameterSetter.setParameter(instance, "start", () -> start, "Query");
  ParameterSetter.setParameter(instance, "length", () -> length, "Query");
  // ... meta parameters for GET and the path "books"
  return instance;
}

and the call reads like a promise chain: onSuccess, onFailed, then send().

DominoRestConfig.initDefaults().addRequestInterceptor(new XsrfInterceptor());

BooksServiceFactory.INSTANCE
    .list(0, 2)
    .onSuccess(page -> {
      log("list    : total=" + page.total + " returned=" + page.books.size());
      for (BookDto b : page.books) {
        log("  book  : #" + b.id + " " + b.title + " (" + b.year + ")");
      }
    })
    .onFailed(failed -> log("list failed: " + failed.getStatusCode()))
    .send();

BooksServiceFactory.INSTANCE
    .get(99)
    .onSuccess(b -> log("get(99) : unexpected " + b.title))
    .onFailed(failed -> log("get(99) : " + failed.getStatusCode() + " " + failed.getBody()))
    .send();

DominoRestConfig.initDefaults() must run before the first request. The interceptor copies the CSRF cookie into the header, using the RequestInterceptor interface that the library documents with a token example:

public class XsrfInterceptor implements RequestInterceptor {
  @Override
  public void interceptRequest(ServerRequest request, ContextWait<ServerRequest> contextWait) {
    String token = Cookies.getCookie("XSRF-TOKEN");
    if (token != null) {
      request.setHeader("X-XSRF-TOKEN", token);
    }
    contextWait.complete(request);
  }
}

ContextWait is org.dominokit.domino.api.shared.extension.ContextAggregator.ContextWait (from domino-aggregator-shared, a transitive dependency). The request metadata in this version has the Java method name but not the HTTP verb (RequestMeta has getMethodName, not getMethod), so this interceptor adds the header to every request. That is harmless on a same-origin GET and the server ignores it, but it must never be used to send the token to another origin.

Compiled with gwt-maven-plugin 1.3.0 and run against the Spring Boot application:

list    : total=6 returned=2
  book  : #1 The Pragmatic Programmer (1999)
  book  : #2 Effective Java (2001)
create  : id=8 Dune
get(99) : 404 {"detail":"No book 99","instance":"/api/books/99","status":404,"title":"Not Found"}

Other features in the README, which I read but did not exercise: dynamic service roots, response interceptors with skipFailHandler, @Retries(timeout, maxRetries), @WithCredentials, multipart forms, custom RequestWriter and ResponseReader, resource locators, and a JVM runtime (domino-rest-jvm) that uses the same interface in server-side tests.

domino-jackson on its own

The mapper can be used without domino-rest, for example with RequestBuilder, fetch or WebSocket text frames:

@JSONMapper
public interface PersonMapper extends ObjectMapper<Person> {}

PersonMapper mapper = PersonMapperImpl.INSTANCE;
Person person = mapper.read("{\"firstName\":\"Ada\",\"lastName\":\"Lovelace\"}");
String json = mapper.write(person);

The snippet is the one in the README (a mapper interface gets a generated PersonMapperImpl, a mapper on a class gets Person_MapperImpl, both with a public INSTANCE); I verified the class-annotation form above, not this interface form. Its annotation list in the README also covers @JSONReader and @JSONWriter for one-way mappers, @JSONRegistration for a generated lookup of mappers, and @CustomSerializer and @CustomDeserializer.

RestyGWT

RestyGWT generates a client from an interface that extends RestService, using a GWT generator (so it runs inside the compiler), with JAX-RS annotations from the old javax.ws.rs namespace, and callbacks as the programming model. Its repository and Central listing show 2.2.7 (2020-05-17) as the last release and 2020-06-30 as the last commit on master.

<dependency>
  <groupId>org.fusesource.restygwt</groupId>
  <artifactId>restygwt</artifactId>
  <version>2.2.7</version>
</dependency>
<dependency>
  <groupId>javax.ws.rs</groupId>
  <artifactId>javax.ws.rs-api</artifactId>
  <version>2.0.1</version>
</dependency>
<dependency>
  <groupId>javax.ws.rs</groupId>
  <artifactId>javax.ws.rs-api</artifactId>
  <version>2.0.1</version>
  <classifier>sources</classifier>
</dependency>
<dependency>
  <groupId>javax.xml.bind</groupId>
  <artifactId>jaxb-api</artifactId>
  <version>2.3.1</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-annotations</artifactId>
  <version>2.9.6</version>
  <classifier>sources</classifier>
</dependency>
<module rename-to="restydemo">
  <inherits name="com.google.gwt.user.User"/>
  <inherits name="org.fusesource.restygwt.RestyGWT"/>

  <source path="resty"/>
  <entry-point class="com.example.bookshelf.resty.RestyDemo"/>
</module>

The restygwt 2.2.7 POM brings gwt-jackson 0.15.4 and the Jackson 2.9.6 jars with it, and was built against GWT 2.7.0; its javax.ws.rs-api 2.0.1 dependency is provided, so the application declares it (with the sources jar, which the GWT compiler reads). The jaxb-api jar is not optional on JDK 21 (the generator refers to javax.xml.bind.annotation.XmlTransient, which the JDK no longer ships) and is the first line of the pitfalls table. With these dependencies, gwt-maven-plugin 1.3.0 packaged the probe.

The DTOs are plain classes (the probe used public fields) and the service is an interface:

public interface BooksResource extends RestService {
  @GET
  @Path("api/books")
  void list(@QueryParam("start") int start, @QueryParam("length") int length, MethodCallback<BookPageDto> callback);

  @GET
  @Path("api/books/{id}")
  void get(@PathParam("id") long id, MethodCallback<BookDto> callback);

  @POST
  @Path("api/books")
  void create(BookDto draft, MethodCallback<BookDto> callback);
}

The client code asks the compiler for an implementation, sets the service root, and registers a dispatcher for the CSRF header:

Defaults.setServiceRoot(GWT.getHostPageBaseURL());
Defaults.setDispatcher((method, builder) -> {
  String token = Cookies.getCookie("XSRF-TOKEN");
  if (token != null) {
    builder.setHeader("X-XSRF-TOKEN", token);
  }
  return builder.send();
});

BooksResource books = GWT.create(BooksResource.class);
books.list(0, 2, new MethodCallback<BookPageDto>() {
  @Override
  public void onSuccess(Method method, BookPageDto page) {
    log("list    : total=" + page.total + " returned=" + page.books.size());
  }

  @Override
  public void onFailure(Method method, Throwable exception) {
    log("list failed: " + method.getResponse().getStatusCode());
  }
});

Without Defaults.setServiceRoot, relative paths are resolved against the module base URL (/restydemo/), not the host page, and the call becomes /restydemo/api/books: Spring answered No static resource restydemo/api/books. (a 404). Output of the full probe against the Spring Boot application:

list    : total=5 returned=2
  book  : #1 The Pragmatic Programmer (1999)
  book  : #2 Effective Java (2001)
get(99) : 404
create  : id=7 Dune

Because the exception text of a failed call has no message in this version (FailedResponseException with an empty message), the status must be read from method.getResponse(). RestyGWT has an extra module system (dispatchers, caching, Resource and Method API, JsonEncoderDecoder) that I did not exercise; the project site and the code are the references.

AutoREST

AutoREST (com.intendia.gwt.autorest) generates a model from a JAX-RS interface whose methods return RxJava 2 types: Single<T> for one JSON object, Observable<T> for a JSON array, Completable for no body. It does not map JSON itself: the GWT module’s RequestResourceBuilder uses XMLHttpRequest and JSON.parse and returns whatever object the parse produced, so DTOs are JsInterop native types, like the Elemental2 probe above. The same interface can be implemented by a JAX-RS server, which is the idea of the project: one definition, a client generated for GWT, the JVM or Android. That does not apply to a Spring MVC server, whose controllers do not use JAX-RS annotations.

<dependency>
  <groupId>com.intendia.gwt.autorest</groupId>
  <artifactId>autorest-gwt</artifactId>
  <version>0.11</version>
</dependency>
<dependency>
  <groupId>com.intendia.gwt.autorest</groupId>
  <artifactId>autorest-processor</artifactId>
  <version>0.11</version>
  <scope>provided</scope>
</dependency>

with the processor also declared in the annotationProcessorPaths of maven-compiler-plugin, as for domino-rest. The artifacts pull in autorest-core, rxjava2-gwt 2.2.20-gwt1 (an RxJava port for GWT), rxjava 2.2.21, Reactive Streams, and the jsr311-api 1.1.1 jar (the JAX-RS 1.1 annotations) with Elemental2 pinned to 1.3.2 as before.

<module rename-to="autorestdemo">
  <inherits name="com.google.gwt.user.User"/>
  <inherits name="com.intendia.gwt.autorest.AutoRest"/>
  <inherits name="elemental2.core.Core"/>

  <source path="autorest"/>
  <entry-point class="com.example.bookshelf.autorest.AutoRestDemo"/>
</module>
@AutoRestGwt
@Path("books")
public interface BooksApi {
  @GET
  Single<BookPageJson> list(@QueryParam("start") int start, @QueryParam("length") int length);

  @GET
  @Path("{id}")
  Single<BookJson> get(@PathParam("id") long id);
}

BooksApi api = new BooksApi_RestServiceModel(() -> new RequestResourceBuilder(GWT.getHostPageBaseURL() + "api"));
api.list(0, 2).subscribe(
    page -> log("list    : total=" + page.total + " returned=" + page.books.length),
    error -> log("list failed: " + describe(error)));

BooksApi_RestServiceModel is the class the processor generates (it implements BooksApi and chains method(GET).path(...).param(...).as(Single.class, ...)), and @AutoRestGwt is com.intendia.gwt.autorest.client.AutoRestGwt. Each @Path is encoded as a single path segment, which has a consequence: a slash inside the annotation value is encoded as %2F. @Path("api/books") made the request /api%2Fbooks, which Spring answered with a 400. Put the prefix in the base URL of RequestResourceBuilder (GWT.getHostPageBaseURL() + "api") and keep @Path to one segment, as above. Output against the Spring Boot application:

get(99) : HTTP 404
list    : total=6 returned=2
  book  : #1 The Pragmatic Programmer (1999)
  book  : #2 Effective Java (2001)

RequestResourceBuilder has a requestFactory hook (a Function that returns the XMLHttpRequest), a requestTransformer and an unexpectedMapper, which are the places to add a CSRF header, retries or error mapping; FailedStatusCodeException#getStatusCode() is how the status is read. The ordering of the two lines get(99) and list is not fixed: the two requests are independent.

gwt-jackson

gwt-jackson is a JSON mapper only, with no HTTP client, based on a GWT generator: an interface that extends ObjectMapper<T> (or ObjectReader<T> or ObjectWriter<T>) is given an implementation by GWT.create. It understands Jackson 2 annotations (@JsonProperty, @JsonCreator, and the list on the project wiki), which makes the DTOs of a Java server that uses Jackson shareable with the client. RestyGWT, GWTP Rest Dispatch, Requestor and gwt-jackson-rest are listed in its README as the frameworks that integrate it for server communication.

<dependency>
  <groupId>com.github.nmorel.gwtjackson</groupId>
  <artifactId>gwt-jackson</artifactId>
  <version>0.15.4</version>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-annotations</artifactId>
  <version>2.9.6</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-annotations</artifactId>
  <version>2.9.6</version>
  <classifier>sources</classifier>
</dependency>
<dependency>
  <groupId>com.squareup</groupId>
  <artifactId>javapoet</artifactId>
  <version>1.0.0</version>
</dependency>
<module rename-to="gjdemo">
  <inherits name="com.google.gwt.user.User"/>
  <inherits name="com.github.nmorel.gwtjackson.GwtJackson"/>

  <source path="gj"/>
  <entry-point class="com.example.bookshelf.gj.GjDemo"/>
</module>

The pairing with GWT’s own RequestBuilder is the one the README lists as "GWT RequestBuilder":

public interface BookPageMapper extends ObjectMapper<BookPageDto> {}
public interface BookMapper extends ObjectMapper<BookDto> {}

BookPageMapper pageMapper = GWT.create(BookPageMapper.class);
BookMapper bookMapper = GWT.create(BookMapper.class);

RequestBuilder rb = new RequestBuilder(RequestBuilder.GET, "/api/books?start=0&length=2");
rb.setHeader("Accept", "application/json");
rb.sendRequest(null, new RequestCallback() {
  @Override
  public void onResponseReceived(Request request, Response response) {
    BookPageDto page = pageMapper.read(response.getText());
    log("list    : total=" + page.total + " returned=" + page.books.size());
    log("write   : " + bookMapper.write(page.books.get(0)));
  }

  @Override
  public void onError(Request request, Throwable exception) {
    log("error: " + exception);
  }
});
list    : total=6 returned=2
  book  : #1 The Pragmatic Programmer (1999)
  book  : #2 Effective Java (2001)
write   : {"id":1,"title":"The Pragmatic Programmer","author":"Andrew Hunt","year":1999,"copies":3}
unknown : com.github.nmorel.gwtjackson.client.exception.JsonDeserializationException: Unknown property 'unknown'

The last line is the mapper’s default: a property that the DTO does not have is an error. Spring Boot’s server-side Jackson accepts unknown properties by default, so a server that adds a field would break a client that uses this default; annotate the DTO with Jackson’s @JsonIgnoreProperties(ignoreUnknown = true) (listed among the annotations the project supports on its wiki; I did not run that annotation) when the server may evolve first.

The javapoet dependency is the one the README names "for the GWT compilation only"; in the probe it arrived with the restygwt POM, so I did not test a build without it. The probe compiled without the JAXB API jar that RestyGWT needed.

Calling third-party JSON services through a server-side proxy

A page cannot call just any JSON API from the browser: the same-origin policy blocks a response from another origin unless that origin sends CORS headers (What is CORS? explains the mechanism). The old workaround, JSONP, runs the response as a script and is described with its risks on the RequestBuilder, JSON & XML page. The safe and still standard answer is a server-side proxy: the browser calls your own origin, and your server calls the third party. This is also a security boundary: API keys for the third party live on the server and never in the compiled JavaScript, and the server decides what comes back.

sequenceDiagram participant B as GWT client participant S as Bookshelf server participant C as Third-party catalogue B->>S: GET /api/isbn/9780132350884 (same origin) Note over S: validates the ISBN, builds the URL itself S->>C: GET /isbn/9780132350884.json (timeouts) C-->>S: JSON with many fields S-->>B: JSON with title and number_of_pages only

A Bookshelf screen can fill a book form from an ISBN. The controller below is the proxy. The browser sends the ISBN; the server builds the upstream URL, calls it with Spring’s RestClient, and returns only the fields the screen needs:

@RestController
public class IsbnProxyController {
  private final RestClient catalogue;

  public IsbnProxyController(RestClient.Builder builder, @Value("${bookshelf.isbn-service}") String baseUrl) {
    this.catalogue = builder.baseUrl(baseUrl).build();
  }

  public record IsbnBook(String title, Integer number_of_pages) {
  }

  @GetMapping("/api/isbn/{isbn}")
  public IsbnBook lookup(@PathVariable @Pattern(regexp = "[0-9]{13}") String isbn) {
    try {
      return catalogue.get().uri("/isbn/{isbn}.json", isbn).retrieve().body(IsbnBook.class);
    } catch (HttpClientErrorException.NotFound e) {
      throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Unknown ISBN");
    } catch (RestClientException e) {
      throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "Catalogue unavailable");
    }
  }
}
bookshelf.isbn-service=http://localhost:8944
spring.http.clients.connect-timeout=2s
spring.http.clients.read-timeout=3s

In the probe, bookshelf.isbn-service pointed at a local stand-in server on port 8944 that served one JSON file with extra fields (publishers, key), so the output is reproducible without depending on a public service. In a real deployment the property holds the base URL of the actual API, and an API key, if there is one, comes from the environment or a secret store. Four requests against the running application (the properties are the spring.http.clients.* ones of the Spring Boot REST client page):

GET /api/isbn/9780132350884   200  {"title":"Clean Code","number_of_pages":464}
GET /api/isbn/9780000000000   404  {"detail":"Unknown ISBN","instance":"/api/isbn/9780000000000","status":404,"title":"Not Found"}
GET /api/isbn/abc             400  {"detail":"Validation failure","instance":"/api/isbn/abc","status":400,"title":"Bad Request"}
(stand-in server stopped)
GET /api/isbn/9780132350884   502  {"detail":"Catalogue unavailable","instance":"/api/isbn/9780132350884","status":502,"title":"Bad Gateway"}

The read timeout was tested with a stand-in that waits six seconds before answering: the request returned the same 502 after 3.1 seconds, which is the read-timeout of 3s.

The rules that make a proxy safe:

Table 5. Rules for a server-side proxy
Rule Why

The client sends an identifier, never a URL

A proxy that fetches whatever URL the browser sends is a server-side request forgery (SSRF) endpoint into your internal network. The controller above builds the URL from a fixed base and a validated ISBN

Validate the identifier strictly

The @Pattern accepts exactly thirteen digits; the abc request never reaches the upstream call

Set connect and read timeouts

A slow third party must not hold your request threads; the two properties above bound the call

Return a DTO of your own, not the upstream body

Upstream fields can change or contain data you do not want to pass on. The response above has two fields of the upstream’s many

Map upstream failures to 502 or 404

The browser must not see upstream stack traces, headers or internal host names

Authenticate and rate-limit the endpoint

It spends your quota with the third party. Apply the same authentication and CSRF rules as the rest of the API

Cache where the terms of the service allow it

It saves quota and latency; keep the cache on the server

The client side of the proxy is one more Rest.json("GET", "/api/isbn/" + isbn, null) with a native type for the two fields. The XSRF, RPC & Server Security page covers XSRF, the same-origin policy and JSON hijacking in more depth than this page.

Security notes

Table 6. Security rules for REST clients in GWT
Risk Rule

Cross-site request forgery

Send the XSRF token header on every state-changing call (POST, PUT, PATCH, DELETE), as in Rest.send, the XsrfInterceptor and the RestyGWT dispatcher above. GWT-RPC has its own token mechanism, described on the XSRF, RPC & Server Security page. Keep SameSite on the session cookie

Authorisation in the client

All checks are on the server. The client code is public and can be edited in the developer tools

Untrusted JSON in the DOM

Put response text in the page with setText, setInnerText or textContent, never setHTML or innerHTML with concatenated data. For rich text use SafeHtml (see SafeHtml & XSS)

Unvalidated JSON shapes

A native JsInterop type is a promise, not a check: a missing property is undefined. Validate fields before using them in URLs, DOM or logic

Unsafe parsers

Use Response.json(), JSON.parse or a mapper. Never eval a response, and avoid JSONP for anything that matters

Entity leakage

Serve DTOs; do not return JPA entities, which can expose columns, lazy relations and internal ids

Secrets in the client

API keys for third-party services stay on the server (proxy section); the compiled JavaScript is readable by every visitor

Token in a URL

Put tokens in headers, not in query strings that end up in logs and the Referer header

Error details

Return problem documents with a stable detail and no stack traces; log the details on the server

Unbounded results

Cap length on the server, as the controller does, and paginate

Pitfalls

Table 7. Common problems with REST and JSON libraries in GWT (messages reproduced on GWT 2.13.1 unless marked)
Symptom Cause Fix

[ERROR] An internal compiler exception occurred with com.google.gwt.dev.jjs.InternalCompilerException: Unexpected error during visit. and Caused by: java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlTransient at GWT.create(BooksResource.class)

The RestyGWT 2.2.7 generator refers to JAXB, which JDK 11 and later no longer include

Add javax.xml.bind:jaxb-api:2.3.1 to the compile class path

RestyGWT calls /restydemo/api/books and the server answers No static resource restydemo/api/books.

The default service root is the module base URL, so a relative @Path is resolved inside the module directory

Defaults.setServiceRoot(GWT.getHostPageBaseURL()), or absolute paths on a configured root

FailedResponseException with an empty message in the callback

The RestyGWT exception carries no text; the status is on the Method

Read method.getResponse().getStatusCode() and getText()

Line 5: No source code is available for type com.github.nmorel.gwtjackson.client.ObjectMapper<T>; did you forget to inherit a required module?

The module file does not inherit com.github.nmorel.gwtjackson.GwtJackson

Add the <inherits> line

com.github.nmorel.gwtjackson.client.exception.JsonDeserializationException: Unknown property 'unknown'

The default is to reject JSON properties the DTO lacks

Add the property, or @JsonIgnoreProperties(ignoreUnknown = true) on the DTO

AutoREST requests return 400 and the URL contains %2F

A slash in @Path("api/books") is encoded as part of one path segment

Keep @Path to one segment and put the prefix in the RequestResourceBuilder base URL

Uncaught TypeError: Cannot read properties of null (reading '_') as soon as a domino-rest call is made (production build, obfuscated)

DominoRestConfig.initDefaults() was never called, so the library has no configuration

Call it once in onModuleLoad, before the first request

mvn dependency:build-classpath lists elemental2-dom 1.1.0 and elemental2-core 1.2.3 although the other pages of this section use 1.3.2

domino-rest-client 2.0.2 depends on those versions and Maven picks the nearest declaration

Pin Elemental2 1.3.2 in dependencyManagement

SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON (Chrome 155 wording)

The URL returned the host page, not JSON: wrong path, or a fallback route

Check the status and the Content-Type header before Response.json()

TypeError: Failed to fetch and, in the console, ... has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

The URL is on another origin (even 127.0.0.1 versus localhost) and the server sends no CORS headers

Serve the API from the same origin, add a proxy, or configure CORS on the server (see the Spring CORS section); do not open CORS to every origin

TimeoutError: signal timed out

AbortSignal.timeout fired before the response arrived

Raise the limit for slow endpoints, or show a retry action

A write returns 401 although the user is signed in, or 403 with an empty body

The CSRF header is missing or does not match the cookie: after a Spring Invalid CSRF token found for http://localhost:8934/api/books/4 in the debug log

Read the XSRF-TOKEN cookie, send it in X-XSRF-TOKEN, and perform a GET first so the cookie exists

HTTP 400 Invalid request content. with no field names

Spring Boot 4.1.1 does not add the violations to the default problem document

Add an @ExceptionHandler that returns the field errors, or handle MethodArgumentNotValidException

Spring Boot fails to start with Parameter 0 of constructor in com.example.bookshelf.api.IsbnProxyController required a bean of type 'org.springframework.web.client.RestClient$Builder' that could not be found.

Spring Boot 4 does not auto-configure RestClient.Builder from the web starter alone

Add spring-boot-starter-restclient

Maven Central says the latest RestyGWT is 1.4

maven-metadata.xml for the artifact was not updated after 2013; the directory has 2.0 to 2.2.7

Pin the version you tested; read the directory listing, not the metadata

A long id loses digits in the browser

JSON numbers are doubles in JavaScript

Send large ids as strings, or keep ids below 2^53

Where older material is outdated

  • JSON and the proxy in GWT in Action. The book (2007, GWT 1.3 and 1.4) devotes its chapter 13 to JSON: the JSON format, the JSONParser and JSONValue classes, and a servlet that proxies a third-party search API for the page. The idea of a server-side proxy is unchanged and is the section above. What changed is everything around it: the server code there uses Apache Commons HttpClient, whose project page says it is end of life and was replaced by Apache HttpComponents; the third-party API it targets is not used here; and GWT now has JsonUtils.safeEval, JSONParser.parseStrict, JsInterop native types and Elemental2 fetch, so a hand-written parse of every value is rarely needed.

  • "A third party library". The Developer Guide still says the application can use JSON with RequestBuilder, JSNI "or a third party library" and does not name any. JSNI is the legacy route (see JSNI and overlay types); native types replace it.

  • XSRF in the Developer Guide. The guide’s XSRF protection is built for GWT-RPC (RpcToken, XsrfProtectedService). For a REST API the mechanism is the one in your server framework (here Spring Security), and the client’s job is to send the token header.

  • RestyGWT’s own home page. It still offers 2.2.0 as the latest download; the latest release on Maven Central is 2.2.7 (2020-05-17). The annotations are the javax.ws.rs ones (javax.ws.rs-api 2.0.1 and its sources on the class path), not the jakarta.ws.rs ones that domino-rest uses.

  • domino-jackson badge. The README says release 1.0.0-RC3; Central has 1.0.6.

What changed recently

  • 2025-09-15. Elemental2 1.3.2, the version used for the fetch probe, is the latest stable release; 1.4.0-RC1 followed on 2026-07-07.

  • 2025-12-30. domino-rest 2.0.0, after release candidates in 2024 (RC1 on 2024-01-14, RC2 on 2024-11-06). 2.0.1 followed on 2026-07-29 and 2.0.2 on 2026-09-24.

  • 2026-07-27. domino-jackson 1.0.6, following 1.0.5 on 2024-11-06.

  • 2023-11-22 to 2023-11-24. AutoREST 0.10 and 0.11, the first releases since 0.9 in 2018.

  • 2026-06-19. GWT 2.13.1, the compiler used here; Jakarta servlet artifacts (gwt-servlet-jakarta) were added in 2.11.0, which is how the Bookshelf server module serves RPC next to the REST API.

  • Spring Boot 4. Uses Jackson 3 (tools.jackson, version 3.1.5 in the probe) for JSON, RestClient for the proxy call, and needs spring-boot-starter-restclient for the auto-configured RestClient.Builder. The annotations that Jackson 3 still reads come from the 2.x annotation artifact (2.21 in the probe).

The files of the probes

The probes were separate throw-away modules next to the Bookshelf modules, one per library; nothing in bookshelf-shared, bookshelf-client or bookshelf-server was changed. The Spring Boot application was a fourth module with the controller, the store, the security configuration, the ISBN proxy and a test profile that turns authentication off:

restdemo/            Elemental2 fetch: Rest, BooksClient, BookJson, BookPageJson, HttpException, Callbacks, RestDemo
libs/domino/         BookDto, BookPageDto, BooksService, XsrfInterceptor, DominoDemo; pom.xml with the processor
libs/resty/          BooksResource, BookDto, BookPageDto, RestyDemo
libs/autorest/       BooksApi, BookJson, BookPageJson, AutoRestDemo; pom.xml with the processor
libs/gj/             BookMapper, BookPageMapper, GjDemo
springdemo/          BookController, BookDto, BookPageDto, BookStore, SecurityConfig, IsbnProxyController

The Elemental2 probe and the other four were compiled with:

mvn -B dependency:build-classpath -Dmdep.outputFile=cp.txt
java -cp "src/main/java:target/generated-sources/annotations:target/classes:$(cat cp.txt)" \
  com.google.gwt.dev.Compiler -sourceLevel 17 -failOnError -war war -workDir wk com.example.bookshelf.DominoDemo

and the Spring Boot application with mvn package and java -jar, with --spring.web.resources.static-locations pointing at the compiled war directories so one origin served the page and the API. The headless run used Chrome’s --dump-dom with a virtual time budget and read the pre element that the probes write their log to.

References