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.
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.
| Option | Latest release on Maven Central | Last commit on the default branch | Compiles and runs on GWT 2.13.1, JDK 21 | Notes |
|---|---|---|---|---|
Elemental2 |
|
repository |
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 |
|
|
2026-09-24 ( |
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 |
|
|
2026-07-27 ( |
yes (through |
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 |
|
2020-06-30 on |
yes, after adding the JAXB API jar (see pitfalls) |
Generator-based, |
AutoREST |
|
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 |
|
2021-02-21 (README edit) |
yes (probe below) |
Generator-based JSON mapper using Jackson 2 annotations; no release since 2018 |
Source: Maven Central — restygwt directory, Maven Central — autorest-gwt directory, GitHub — DominoKit/domino-rest, GitHub — nmorel/gwt-jackson
Reading the table:
-
Pick by the maintenance signal you need.
domino-restanddomino-jacksonpublished 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:
| Probe | Bytes | Bytes gzipped | What it contains |
|---|---|---|---|
Elemental2 |
31,179 |
10,626 |
the full CRUD probe of this page, GWT |
|
134,473 |
43,117 |
list, get and create calls, two mappers, one request interceptor, |
RestyGWT |
52,726 |
18,039 |
list, get and create calls, one dispatcher |
gwt-jackson with |
75,392 |
25,769 |
two mappers and one |
AutoREST |
51,398 |
17,663 |
list and get calls with RxJava |
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.
| Verb | Path | Success | Failure |
|---|---|---|---|
GET |
|
200, |
none expected; |
GET |
|
200, one book |
404 |
POST |
|
201 with a |
400 for a body that fails validation; 401 or 403 without authentication or without the CSRF header |
PUT |
|
200, the saved book |
404, 400, 401 or 403 |
DELETE |
|
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.
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>
Source: GitHub — google/elemental2
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;
}
Source: GWT — Coding Basics: JsInterop
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)).fetchhas no timeout of its own.AbortSignal.timeoutaborts the request after the delay and rejects the promise with aTimeoutError(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.getCookieis the GWT class (com.google.gwt.user.client.Cookies); Elemental2 has no cookie API beyond thedocument.cookiestring. 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.
fetchrejects only for network failures and aborts. The wrapper turns a response withok == falseinto a rejection of its own type,HttpException, which carries the status and thedetailof the problem document. -
The Java exception is the rejection value.
Promise.reject(new HttpException(...))rejects with the Java object, so a later handler can testerror instanceof HttpException. I did not test athrowinside a callback; the explicitrejectis 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);
}
}
Source: MDN — Using the Fetch API
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;
});
Source: MDN — Using the Fetch API
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 tokenis a403with 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 jsonis 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’sResponse.json()failure, not a Java exception. -
cross originis a call to the same server through a different host name (127.0.0.1from a page loaded fromlocalhost). The browser blocked it, and loggedAccess 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 learnsTypeError: 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;
});
}
Source: GWT Javadoc — AsyncCallback
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:
| Way | When | Limits |
|---|---|---|
Native JsInterop type ( |
the usual choice; zero code per property |
no validation: a missing field is |
|
a payload whose shape you do not control, or a map of unknown keys |
every access is a string key and an |
|
old code, and the |
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>
Source: GitHub — DominoKit/domino-rest
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);
}
}
Source: GitHub — DominoKit/domino-rest
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());
}
});
Source: RestyGWT — User guide: configuring service root URLs, RestyGWT — User guide: creating custom request dispatchers
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)));
Source: GitHub — intendia-oss/autorest
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);
}
});
Source: GitHub — nmorel/gwt-jackson
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.
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:
| 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 |
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
| Risk | Rule |
|---|---|
Cross-site request forgery |
Send the XSRF token header on every state-changing call (POST, PUT, PATCH, DELETE), as in |
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 |
Unvalidated JSON shapes |
A native JsInterop type is a promise, not a check: a missing property is |
Unsafe parsers |
Use |
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 |
Error details |
Return problem documents with a stable |
Unbounded results |
Cap |
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
|
The RestyGWT 2.2.7 generator refers to JAXB, which JDK 11 and later no longer include |
Add |
RestyGWT calls |
The default service root is the module base URL, so a relative |
|
|
The RestyGWT exception carries no text; the status is on the |
Read |
|
The module file does not inherit |
Add the |
|
The default is to reject JSON properties the DTO lacks |
Add the property, or |
AutoREST requests return 400 and the URL contains |
A slash in |
Keep |
|
|
Call it once in |
|
|
Pin Elemental2 1.3.2 in |
|
The URL returned the host page, not JSON: wrong path, or a fallback route |
Check the status and the |
|
The URL is on another origin (even |
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 |
|
|
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 |
Read the |
|
Spring Boot 4.1.1 does not add the violations to the default problem document |
Add an |
Spring Boot fails to start with |
Spring Boot 4 does not auto-configure |
Add |
Maven Central says the latest RestyGWT is 1.4 |
|
Pin the version you tested; read the directory listing, not the metadata |
A |
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
JSONParserandJSONValueclasses, 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 hasJsonUtils.safeEval,JSONParser.parseStrict, JsInterop native types and Elemental2fetch, 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.rsones (javax.ws.rs-api2.0.1 and its sources on the class path), not thejakarta.ws.rsones thatdomino-restuses. -
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
fetchprobe, is the latest stable release; 1.4.0-RC1 followed on 2026-07-07. -
2025-12-30.
domino-rest2.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-jackson1.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,RestClientfor the proxy call, and needsspring-boot-starter-restclientfor the auto-configuredRestClient.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.