JSNI & Overlay Types

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 about legacy technology. The Developer Guide page on JSNI opens with the advice to use JsInterop for new implementations and states that "JSNI will be removed with GWT 3"; the same pages are kept for the large amount of existing GWT code that still contains native methods and overlay types. It is written against GWT 2.13.1 (released 2026-06-19), gwt-maven-plugin 1.3.0, JDK 21 running the compiler with -sourceLevel 17 and the Jakarta Servlet 6 server of the Bookshelf scenario, checked on 2026-10-08. It concerns client code only (Java sources that the compiler turns into JavaScript): JSNI cannot appear in code that runs on the server, and the section on migration shows the JsInterop replacement of every construct, which is what you should write today. Every behaviour, error message and generated fragment quoted below was produced by compiling a small lab module with the 2.13.1 compiler and running it in a GWTTestCase (HtmlUnit, production-mode JavaScript); the compiler messages are quoted as the tool printed them.

Why this page exists

GWT compiles Java to JavaScript, but real applications need to talk to the rest of the browser: a charting library loaded with a <script> tag, a global that the host page defines, a browser API that no GWT class wraps. Before JsInterop (introduced in GWT 2.8) there were two tools for this, and both are still supported by 2.13.1:

  • JSNI (JavaScript Native Interface): a native Java method whose body is JavaScript written inside a special comment. The compiler injects the JavaScript inline. It works at the level of single methods.

  • Overlay types: Java classes that extend com.google.gwt.core.client.JavaScriptObject and describe an existing JavaScript object, so that the rest of your code can use a typed Java API (with completion and refactoring) on data that never was a Java object. GWT’s own DOM classes are overlay types: in the 2.13.1 sources com.google.gwt.dom.client.Element extends Node and Node extends JavaScriptObject.

The history that explains why you meet them (all from the official release notes):

Table 1. JSNI, overlays and JsInterop in the release notes
Release What the notes say

2.7.0

JSNI references no longer need fully qualified class names when Java would not need them either (imports work). JsInterop is "not final and there are bugs", so "production GWT apps and libraries should continue to use JSNI for now"; the experimental flag is -XjsInteropMode.

2.8.0 (RC1)

Classic Dev Mode (the mode in which a JSNI type mismatch threw a HostedModeException) is deprecated. The breaking-change list includes "JSNI disallows method references to Double, String, Boolean and its super types" (issue 9356). JsInterop becomes a core feature of 2.8, as the JsInterop page of the Developer Guide says.

2.8.2

"Migrated lang/jre emulation JSNI to JsInterop to share with J2CL." GWT’s own library moved away from JSNI.

2.13.1

JSNI and overlays still compile and behave as described below. No warning is printed for either.

So, for new code use JsInterop (see JsInterop) with native @JsType types and @JsOverlay; for existing code read on, and use the migration table when you decide to replace it.

Writing native methods (JSNI)

A JSNI method is declared native and has its JavaScript body in a comment block between the closing parenthesis of the parameter list and the semicolon. The block starts with the exact token /*-{ and ends with the exact token }-*/. Static and instance methods are both allowed; in an instance method this is the Java object (or the JavaScript object, for an overlay type).

A JSNI method and a call to it
package com.example.bookshelf.lab.client;

public final class Browser {
  private Browser() {}

  /** $doc is the host page's document, not the nested frame's. */
  public static native String pageTitle() /*-{
    return $doc.title;
  }-*/;

  public static native void log(String message) /*-{
    $wnd.console.log(message);
  }-*/;

  /** Reads a global the host page defined; undefined arrives in Java as null. */
  public static native String config(String key) /*-{
    var cfg = $wnd.bookshelfConfig;
    return cfg ? cfg[key] : null;
  }-*/;
}

The compiler checks the syntax of the JavaScript body, then generates the glue that converts arguments and results, and the output simply contains the body in place of a call. With -style PRETTY the three methods above are inlined into the code that uses them. The Bookshelf lab entry point calls Browser.config("payload") and Browser.log(...), and the generated code is:

Generated JavaScript (2.13.1, -style PRETTY): the JSNI bodies were inlined
function log_0(message){
  $wnd.console.log(message);
}
...
function $onModuleLoad(){
  var books, i, n, cfg;
  books = parseAll((cfg = $wnd.bookshelfConfig , cfg?cfg['payload']:null));
  for (i = 0 , n = books.length; i < n; i++) {
    log_0(books[i].title + ' (' + books[i].author + ', ' + books[i].year + ')');
  }
  $export(new Cart(40));
}

Rules and consequences:

  • Client code only. native methods are ordinary Java native methods on the JVM. A class that mixes JSNI into code shared with the server fails on the server the first time the method is called:

    Calling a JSNI method on the JVM
    Exception in thread "main" java.lang.UnsatisfiedLinkError: 'java.lang.String T.where()'
    	at T.where(Native Method)
    	at T.main(T.java:3)

    Keep JSNI in client-only packages, and in shared code use GWT.isClient() guards or put the native method behind an interface with a client and a server implementation (see JRE Emulation & Shared Code).

  • The body is JavaScript, not Java. The Java compiler does not look inside the comment. The GWT compiler parses it (see the next subsection) and rewrites references, but it cannot know whether return "Not A Number" fits an int return type (see type mapping).

  • Varargs. A Java varargs call between two Java pieces of code works as usual, but JavaScript code called from Java receives the arguments as one array (and a Java varargs method called from JSNI expects an array of the right type). The lab method join(String... parts) received parts.length of 3 and typeof parts of object for join("a", "b", "c").

  • Debugging. Java tools cannot step through a JSNI body, because it is JavaScript. The Developer Guide’s advice (written for classic Dev Mode) is to set a breakpoint on the line that holds the opening brace of the method to see its arguments; in the browser the compiled JavaScript, JSNI bodies included, is ordinary script for the developer tools.

  • It is the web equivalent of inline assembly, in the words of the Developer Guide: potentially less portable across browsers, more likely to leak memory, harder for tools to follow and harder for the compiler to optimise. Generated identifiers are renamed in the output, but whatever JavaScript names you write inside the body (such as FirstName of a JSON object) are never obfuscated.

The JavaScript dialect is ES5

The body is parsed by GWT’s own JavaScript parser, a fork of Mozilla’s Rhino parser (com.google.gwt.dev.js.rhino.Parser in the compiler sources). It understands ECMAScript 5 and nothing newer, whatever the browser would accept, and that is the first thing people hit when they paste modern code into a native method. Each construct below was put in its own native method of the lab module and compiled with -failOnError -validateOnly:

Table 2. ES2015+ syntax rejected inside a JSNI body (GWT 2.13.1)
Construct Message printed by the compiler Write instead

const a = 1;

identifier is a reserved word

var a = 1;

let a = 1;

missing ; before statement

var a = 1;

[1].map(x => x)

syntax error

[1].map(function(x) { return x; })

template literal with backquotes

illegal character

string concatenation with +

class A {}

identifier is a reserved word

a constructor function, or (better) a Java class

$wnd?.foo

syntax error

$wnd && $wnd.foo

Math.max(...[1,2])

syntax error

Math.max.apply(null, [1,2])

for (var x of [1]) {}

missing ; after for-loop initializer

for (var i = 0; i < a.length; i++)

function f(a = 1) {}

missing ) after formal parameters

test arguments.length or a === undefined

var {a} = obj;

missing variable name

var a = obj.a;

Regular expression literals, JSON.parse, trailing commas in array literals and ordinary function expressions compile. A pragmatic consequence: if the JavaScript you need to call is modern, call it from a file you load with a <script> tag (or ScriptInjector) and keep only a thin ES5 call in the native method, or bind it with JsInterop, which has no such restriction because it generates no JavaScript text of its own.

Errors are easy to miss without -failOnError

A JSNI error is a compilation error of the unit that contains the method. By default the compiler does not stop; it ignores the broken unit and keeps going, and when the broken unit is reachable you get a short message that hides the details:

Default compiler output when units contain errors
Ignored 5 units with compilation errors in first pass.
Compile with -strict or with -logLevel set to TRACE or DEBUG to see all errors.
Finding entry point classes
   Tracing compile failure path for type 'com.example.bookshelf.bad.BadEntry'
      [ERROR] Errors in 'file:.../bad/BadEntry.java'
         [ERROR] Line 16: 'new' cannot be used to create instances of JavaScriptObject subclasses; instances must originate in JavaScript

Use -failOnError (alias -strict) to get all the errors of every unit, which is how every message in the pitfalls table was collected; it is the right setting for CI. See Compiler & Compilation for the flag.

$wnd and $doc

A compiled GWT module does not run in the host page’s own global scope. The standard cross-site (xsiframe) bootstrap (see Linkers & Bootstrap) loads the compiled code in a nested iframe, so inside the module window and document are the frame’s, not the page’s. The generated JavaScript begins with the lines that define the two variables you use in JSNI instead:

The first lines of the 2.13.1 *.cache.js file
var $wnd = $wnd || window.parent;
var __gwtModuleFunction = $wnd.lab;
...
var $doc = $wnd.document;

The lab method

public static native String frames() /*-{
  return "window===$wnd: " + (window === $wnd) + ", window.parent===$wnd: " + (window.parent === $wnd)
      + ", document===$doc: " + (document === $doc) + ", $doc.body: " + ($doc.body ? "yes" : "no");
}-*/;

printed, in the test:

window===$wnd: false, window.parent===$wnd: true, document===$doc: false, $doc.body: yes
flowchart TB subgraph host["Host page (index.html)"] hw["window = $wnd
document = $doc
window.bookshelfConfig, console, the DOM you see"] subgraph frame["Nested iframe loaded by the bootstrap"] code["Compiled GWT JavaScript
and your JSNI bodies
window and document here are the frame's"] end end code -- "$wnd, $doc" --> hw hw -- "$entry(function) installed on $wnd" --> code

The rules that follow:

  • Always write $wnd.alert(...), $doc.title, $wnd.bookshelfConfig; a bare window.bookshelfConfig reads the iframe’s window, where nothing is defined (for the config lookup above that would be undefined, which reaches Java as null).

  • Globals created for the page ($wnd.cartAdd = ...) are visible to every script of the page; names you create on the frame’s window are not.

  • GWT’s own wrappers do exactly this: Window.alert, Window.confirm and Window.prompt in the 2.13.1 sources are one-line native methods that call $wnd.alert(msg), $wnd.confirm(msg) and $wnd.prompt(msg, initialValue) and return the result. They are the smallest possible example of "wrap a type-safe Java signature around existing JavaScript".

  • The DOM that the user sees is the one behind $doc. Use com.google.gwt.dom.client.Document.get() (an overlay type) instead of $doc when a GWT class exists; the page DOM & Elemental2 covers it.

  • In JsInterop you do not write $wnd: a @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "console") binds to the page’s global scope, and the compiler emits $wnd.console.log(...) for you (verified in the output of the migration example below).

Calling Java from JavaScript

JavaScript is dynamically typed and the Java it calls is statically typed and renamed by the compiler, so a Java member is named in JSNI with a special syntax that the compiler resolves while it processes the body (borrowing the JNI idea of mangled signatures).

Method, constructor and field references

Table 3. JSNI reference syntax
Form Meaning

[instance-expr.]@class-name::method-name(param-signature)(arguments)

Call a method. instance-expr. is present for an instance method and absent for a static one. class-name is the class declaring the method (or a subclass). param-signature is the JNI signature of the parameter types, without the return type.

@pkg.TopLevel::new()()

Call a constructor: the method name is always new.

@pkg.TopLevel.StaticInner::new()()

A static nested class is named with dots.

@pkg.TopLevel.InstanceInner::new(Lpkg/TopLevel;I)(outer, 123)

The enclosing instance of an inner (non-static) class is the first parameter of the constructor signature.

[instance-expr.]@class-name::field-name

Read a field; assign to it with =. No parentheses.

The parameter signature uses the JNI type codes: Z boolean, B byte, C char, S short, I int, J long, F float, D double, Ljava/lang/String; for a class (slashes, and a trailing semicolon), and a leading [ for each array dimension. The Developer Guide links the JNI specification for the full table.

Two conveniences are worth knowing, both confirmed by compiling them:

  • Wildcard signature (*). When the method name is not overloaded, the signature can be written (*). GWT’s own JsonUtils uses @JsonUtils::escapeChar(*)(x, escapeTable) in 2.13.1. When two overloads exist the compiler refuses:

    [ERROR] Line 15: Referencing method 'com.example.bookshelf.bad3.E.many(*)': ambiguous wildcard match; both 'static com.example.bookshelf.bad3.E.many(Ljava/lang/String;)' and 'static com.example.bookshelf.bad3.E.many(I)' match
  • Short class names. Since 2.7 a reference can use the simple name where Java would allow it (same package or an import). A reference @E::one(*)("...") in the lab compiled without errors.

Java members reached from JSNI, and a Java function published to the page
package com.example.bookshelf.lab.client;

public class Cart {
  public static int created;
  public int total;
  public String owner = "anonymous";

  public Cart() { created++; }
  public Cart(int initial) { this(); total = initial; }

  public int add(int amount) { total += amount; return total; }
  public String add(String label) { return owner + ":" + label; }
  static String label(String s, double d) { return s + "/" + d; }

  /** Exposes this cart to host-page scripts; $entry installs the uncaught exception handler. */
  public native void export() /*-{
    var that = this;
    $wnd.cartAdd = $entry(function(amount) {
      return that.@com.example.bookshelf.lab.client.Cart::add(I)(amount);
    });
    $wnd.cartLabel = $entry(@com.example.bookshelf.lab.client.Cart::label(Ljava/lang/String;D));
  }-*/;

  public static native Cart make() /*-{
    return @com.example.bookshelf.lab.client.Cart::new(I)(40);
  }-*/;

  public static native int touch(Cart a, Cart b) /*-{
    a.@com.example.bookshelf.lab.client.Cart::owner = "alice";
    var t = a.@com.example.bookshelf.lab.client.Cart::total;
    t += b.@com.example.bookshelf.lab.client.Cart::add(Ljava/lang/String;)("x").length;
    @com.example.bookshelf.lab.client.Cart::created = @com.example.bookshelf.lab.client.Cart::created + 100;
    return t + a.@com.example.bookshelf.lab.client.Cart::add(I)(2);
  }-*/;
}

The test around it printed (HtmlUnit, production JavaScript):

make total=40 created=1
touch=93
owner=alice created=102
cartAdd(5)=47
cartLabel=a/1.5
total=47

touch is 40 (the total of a) plus 11 (the length of "anonymous:x", because b still has its default owner) plus 42 (a.add(2)), and it left created at 102 after the assignment to the static field. The two overloads of add were told apart by the signature (I against Ljava/lang/String;).

Misspelled references are compile errors, which is the main safety net JSNI offers (these are the messages of the 2.13.1 compiler for the lab class BadRef):

[ERROR] Line 12: Referencing class 'com.example.bookshelf.bad.NoSuchClass': unable to resolve class
[ERROR] Line 9: Referencing method 'com.example.bookshelf.bad.BadRef.real(I)': unable to resolve method in class 'com.example.bookshelf.bad.BadRef'
[ERROR] Line 6: Referencing method 'com.example.bookshelf.bad.BadRef.missing()': unable to resolve method in class 'com.example.bookshelf.bad.BadRef'
[ERROR] Line 15: Referencing field 'com.example.bookshelf.bad.BadRef.nope': unable to resolve field in class 'com.example.bookshelf.bad.BadRef'

Handing Java to handwritten JavaScript: $entry

Code that is not produced by the GWT compiler (a script on the page, a third-party library calling back, a setTimeout) cannot call Java directly, because the compiler renames Java members and the Java methods do not exist under their source names. The generated code for Cart.add shows it: the instance method was emitted as add_0, and a call from JSNI was compiled to that.add_0(amount):

Generated JavaScript for Cart.export() (2.13.1, -style PRETTY)
function $export(this$static){
  var that = this$static;
  $wnd.cartAdd = $entry(function(amount){
    return that.add_0(amount);
  }
  );
  $wnd.cartLabel = $entry(label_0);
}

So the pattern to publish Java behaviour is: write a JSNI method (typically called once from the entry point) that assigns a JavaScript function to a global on $wnd, and wrap that function in $entry(...). $entry is an implicitly defined function; the Developer Guide describes it as the way to make sure the Java-derived code runs with the uncaught-exception handler installed and with the deferred utility services pumped (it is re-entrant and should be used wherever GWT-derived JavaScript can be called from a non-GWT context). In the 2.13.1 output the definition is var $entry = entry; with

function entry0(jsFunction, thisObj, args){
  var initialEntry;
  initialEntry = enter();
  try {
    return apply_0(jsFunction, thisObj, args);
  }
   finally {
    exit(initialEntry);
  }
}

and exit flushes the finally commands of the scheduler on the outermost call. The Javadoc of the runtime method behind it (com.google.gwt.core.client.impl.Impl.entry) says it should be used whenever GWT code is entered from a JavaScript context with no GWT code of the same module on the call stack (event handlers, exported methods, module initialization), and that the wrapped function returns undefined if the uncaught exception handler catches an exception it raised. Leave it out and a Java exception thrown from your callback is a raw JavaScript error for the page, with the handler registered through GWT.setUncaughtExceptionHandler bypassed.

Two details from the Developer Guide matter for instance methods: take var that = this; first (JavaScript this inside the callback is not the Java object), and then call that.@pkg.Account::add(I)(amt).

Publishing a small JavaScript API (window.bookshelfApi) for the host page
public final class Api {
  private static int lastOpened = -1;

  private Api() {}

  static void open(int id) { lastOpened = id; }
  public static int lastOpened() { return lastOpened; }

  public static native void install() /*-{
    $wnd.bookshelfApi = {
      open: $entry(function(id) {
        @com.example.bookshelf.lab.client.Api::open(I)(id);
      }),
      version: "1.0"
    };
  }-*/;
}

Call Api.install() from onModuleLoad. A page script can then run bookshelfApi.open(7); in the test the call $wnd.bookshelfApi.open(7) ran the Java method and Api.lastOpened() returned 7.

Type mapping across the boundary

Parameters and return values of a JSNI method are declared with Java types, and the rules for what crosses the boundary apply both to ordinary calls of a native method and to the @Class::method calls made from JavaScript.

Table 4. Java values passed into JavaScript
Java type How it appears to JavaScript

String

a JavaScript string

boolean

a JavaScript boolean

long

disallowed (see long)

the other numeric primitives (byte, short, char, int, float, double)

a JavaScript number

JavaScriptObject (and subclasses)

the original JavaScript object, which must have come from JavaScript code in the first place

a Java array

an opaque value that can only be passed back to Java

any other Java Object

an opaque value, accessible only through the @Class::member syntax

Table 5. JavaScript values passed into Java code
Declared Java type What JavaScript must return or pass

String

a JavaScript string

boolean

a JavaScript boolean

long

disallowed

a numeric primitive

a JavaScript number

JavaScriptObject

a native JavaScript object, for example document.createElement("div")

any other Java Object (including arrays)

a Java object of the right type that originated in Java code; JavaScript cannot construct Java objects "from thin air"

null is the same value in both languages and is legal for any non-primitive Java type; JavaScript undefined arrives in Java as null as well (a difference from older GWT versions in which undefined was illegal). JsArray, JsArrayBoolean, JsArrayInteger, JsArrayNumber, JsArrayString (and JsArrayMixed) in com.google.gwt.core.client are overlay types around a native JavaScript array, the efficient way to pass sequences.

The compiler does not check the types: what actually happens in production mode

The Developer Guide shows a HostedModeException for a method that returns a string from an int method, and notes that this happens in Development Mode only, never in production mode. With classic Dev Mode gone (see Development Mode & Debugging), production-mode JavaScript is what you get in Super Dev Mode, GWTTestCase and the deployed app, and nothing checks the value. These are the actual results from the 2.13.1 lab, on JavaScript produced by the compiler:

Table 6. Wrong JSNI return values in production mode
Native method body Declared Java return type Value seen by Java

return "Not A Number";

int

the string Not A Number (printing the "int" gives that text)

return 3.7;

int

3.7, not truncated to 3

return 4294967297;

int

4294967297, no 32-bit wrap

return "yes";

boolean

the string yes, neither true nor false

return undefined;

String

null (Browser.undefinedString() == null was true)

return d / 2; with argument 5

double

2.5

Other observations from the same run: typeof of a Java String inside JavaScript was string; of a boxed Integer and of a Cart it was object (so a boxed Integer is not a JavaScript number, unlike an int); a Java String[] was an object with length 2 in the compiled output (the Developer Guide calls arrays opaque, and nothing promises that representation, so do not rely on it).

A long never crosses

A Java long is emulated by the compiler (see JRE Emulation & Shared Code) as an opaque value, so JSNI cannot do arithmetic on it, and by default the compiler forbids any access:

A long parameter, return type or field reference in a JSNI method (2.13.1)
[ERROR] Line 8: Referencing field 'com.example.bookshelf.bad.LongJsni.counter': type 'long' is not safe to access in JSNI code
[ERROR] Line 4: Parameter 'id': type 'long' is not safe to access in JSNI code
[ERROR] Line 4: Type 'long' may not be returned from a JSNI method

The official page "Restrictions on Long Values with JSNI" (gwt-dev-help/longJsniRestriction.html under the Developer Guide path doc/latest) lists the ways out:

  • for numbers that fit into a double, use double;

  • for computations that need the full long semantics, do them in Java;

  • for values meant to be handed back to Java unchanged, wrap them in java.lang.Long (no restriction on Long in JSNI);

  • if you really mean it, annotate the method with com.google.gwt.core.client.UnsafeNativeLong: the long is allowed through, it stays opaque, and all JavaScript can do is pass it back to Java. The annotation has SOURCE retention and the lab method @UnsafeNativeLong public static native long pass(long id) compiled without a message.

Exceptions across the boundary

An exception can start in Java or in JavaScript, and may travel through a native method.

  • JavaScript to Java. When a JavaScript exception leaves a native method and is caught by a Java catch, it is wrapped as com.google.gwt.core.client.JavaScriptException at that moment; before that it is still a plain JavaScript object and JavaScript code can catch it normally. The wrapper carries the JavaScript error’s name and description (getName(), getDescription(), getMessage(), and getThrown() for the original thrown value).

  • Java through JavaScript. A Java exception thrown by Java code called from a native method propagates through the native method and can be caught in the Java caller with its type and identity intact.

  • Best practice (the Developer Guide’s): handle JavaScript exceptions in JavaScript and Java exceptions in Java.

Throwing from native methods, and catching in Java
public static native void jsThrowsError() /*-{
  throw new Error("catalogue unavailable");
}-*/;

public static native void jsThrowsString() /*-{
  throw "plain string";
}-*/;

public static native void jsTypeError() /*-{
  null.foo();
}-*/;

static void fooImpl() { throw new StockException("out of stock"); }

public static native void nativeFoo() /*-{
  @com.example.bookshelf.lab.client.Errors::fooImpl()();
}-*/;

public static String describe(Runnable r) {
  try {
    r.run();
    return "no exception";
  } catch (JavaScriptException e) {
    return "JavaScriptException name=" + e.getName() + " description=" + e.getDescription()
        + " message=" + e.getMessage();
  } catch (RuntimeException e) {
    return e.getClass().getName() + ": " + e.getMessage();
  }
}

Results in the test (the TypeError text comes from the HtmlUnit JavaScript engine; real browsers word it differently):

JavaScriptException name=Error description=: catalogue unavailable message=(Error) : catalogue unavailable
JavaScriptException name=String description=: plain string message=(String) : plain string
JavaScriptException name=TypeError description=: Cannot call method "foo" of null message=(TypeError) : Cannot call method "foo" of null
com.example.bookshelf.lab.client.Errors$StockException: out of stock
caught Error: handled in JS

Notes from the lab: the description starts with a colon and a space because JavaScriptException builds it as the constructor’s description (empty here), ": ", and the error’s message; getMessage() is "(" + name + ") " + description, as in the 2.13.1 source. Throwing a JavaScript string gives name String. The StockException thrown inside fooImpl came out of nativeFoo() as the very same Java type. The Javadoc adds that the stack trace of a JavaScriptException varies by browser and can be empty; fillInStackTrace() rebuilds one for the catch site.

Overlay types

JSNI works method by method. Overlay types raise the level: you subclass JavaScriptObject, and the Java type is the JavaScript object, with Java methods that read it. The Developer Guide’s JSON example is the canonical one; here it is for the Bookshelf, whose BooksRestServlet returns {"total":25,"books":[{"id":1,"title":"...","author":"...","year":2001,"copies":3}, ...]}.

An overlay type for one book of the /api/books payload
package com.example.bookshelf.lab.client;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.JsonUtils;

/** Overlay of one element of the /api/books JSON payload. */
public final class BookJso extends JavaScriptObject {
  protected BookJso() {}

  public static BookJso parse(String json) {
    return JsonUtils.safeEval(json);
  }

  public static native JsArray<BookJso> parseAll(String json) /*-{
    return JSON.parse(json).books;
  }-*/;

  public native int getId() /*-{ return this.id; }-*/;
  public native String getTitle() /*-{ return this.title; }-*/;
  public native String getAuthor() /*-{ return this.author; }-*/;
  public native int getYear() /*-{ return this.year; }-*/;
  public native int getCopies() /*-{ return this.copies; }-*/;

  public String describe() {
    return getTitle() + " (" + getAuthor() + ", " + getYear() + ")";
  }

  public boolean inStock() {
    return getCopies() > 0;
  }
}

How you obtain an instance is the point: you cannot write new BookJso(), because the whole idea is to put a Java type on an object that already exists. It comes out of a JSNI method, or from a JSON parser, and the Java type is taken from the declared return type:

JsArray<BookJso> books = BookJso.parseAll(json);
for (int i = 0; i < books.length(); i++) {
  BookJso b = books.get(i);
  Window.alert(b.describe() + " in stock: " + b.inStock());
}

With the two-element payload of the test the loop produced Effective Java (Joshua Bloch, 2001) inStock=true and Clean Code (Robert Martin, 2008) inStock=false. JsonUtils.safeEval(String) (in 2.13.1: JSON.parse wrapped to throw an IllegalArgumentException with the text Error parsing JSON: ...) returns any overlay type; JsonUtils.unsafeEval uses eval after escaping and JsonUtils.safeToEval checks that a text only contains JSON syntax. Prefer safeEval/JSON.parse; eval of server data is a security risk (see RequestBuilder, JSON & XML).

The Developer Guide’s Customer example uses the same shape, and its generic JsArray<E extends JavaScriptObject> is what GWT ships as com.google.gwt.core.client.JsArray.

Why overlays are cheap

Overlay methods cannot be overridden, so every call is statically resolvable and the compiler emits a direct, inlinable call. Look at what became of describe(), getTitle() and the others in the 2.13.1 output of the Bookshelf lab:

function parseAll(json){
  return JSON.parse(json).books;
}
...
function $onModuleLoad(){
  var books, i, n, cfg;
  books = parseAll((cfg = $wnd.bookshelfConfig , cfg?cfg['payload']:null));
  for (i = 0 , n = books.length; i < n; i++) {
    log_0(books[i].title + ' (' + books[i].author + ', ' + books[i].year + ')');
  }
  ...
}

books.get(i), describe() and every getter were inlined down to property reads (books[i].title); there is no wrapper object, no per-instance allocation and no dispatch. The properties title, author and year are not renamed, because they come from JavaScript. That is the whole appeal of overlays and it is also why their definition is tight.

The rules for JavaScriptObject subclasses

The compiler enforces them with JSORestrictionsChecker; each rule below produced the quoted message with 2.13.1 (the lab class Jsos violates all of them at once):

Table 7. Overlay type restrictions and the compile errors
Rule Message

Constructors must be protected

Constructors must be 'protected' in subclasses of JavaScriptObject

Constructors must have no parameters

Constructors must not have parameters in subclasses of JavaScriptObject

Constructors must be completely empty

Constructors must be totally empty in subclasses of JavaScriptObject

No instance fields (static fields are fine)

Instance fields cannot be used in subclasses of JavaScriptObject

Instance methods must be final (or the class final, or the method private)

Instance methods must be 'final' in non-final subclasses of JavaScriptObject

Nested overlay classes must be static

Nested classes must be 'static' if they extend JavaScriptObject

Never instantiate with new

'new' cannot be used to create instances of JavaScriptObject subclasses; instances must originate in JavaScript

One overlay per interface (see below)

Only one JavaScriptObject type may implement the methods of an interface that declared methods. The interface (com.example.bookshelf.bad.Jsos$Named) is implemented by both (com.example.bookshelf.bad.Jsos$NamedOne) and (com.example.bookshelf.bad.Jsos$NamedTwo)

Also: JavaScriptObject itself declares equals, hashCode and toString as final, so an overlay cannot override them (the Java compiler says Cannot override the final method from JavaScriptObject). equals calls an equals property of the JavaScript object if there is one and otherwise compares by JavaScript identity; hashCode uses a hashCode property or a counter assigned to the object. The checker source also has a message Methods cannot be overridden in JavaScriptObject subclasses; a final overlay class that implemented an interface method without final compiled in the lab without any message, so the final-method rule is the one you meet in practice.

Overlays that implement interfaces

Since GWT 2.0 an overlay may implement an interface, which allows code (and tests) to work with either a JavaScript-backed or a normal Java implementation. Every method of the interface may be mapped to at most one overlay type, because an overlay has no runtime type information and the call could not be dispatched unambiguously:

public interface Named {
  String getName();
}

public final class PersonJso extends JavaScriptObject implements Named {
  protected PersonJso() {}

  public static native PersonJso create(String name) /*-{
    return {name: name};
  }-*/;

  @Override
  public native String getName() /*-{ return this.name; }-*/;
}

public class PersonImpl implements Named {
  private final String name;
  public PersonImpl(String name) { this.name = name; }
  @Override public String getName() { return name; }
}

The test called getName() through the Named type on both implementations and got Ada and Grace; a second overlay class implementing Named gave the "Only one JavaScriptObject type" error of the table.

No runtime types: instanceof and casts

Because a JavaScript object has no Java class, the only runtime check available is "is it a JavaScript object at all?". The lab results:

Table 8. Runtime behaviour of overlay types
Test Result

plain instanceof BookJso for JavaScriptObject.createObject()

true

plain instanceof PersonJso for the same object

true

new Cart() instanceof JavaScriptObject

false

(BookJso) "str"

ClassCastException

(BookJso) new Cart()

ClassCastException

plain.<BookJso>cast() == plain

true

So an overlay type is a claim about the shape of the data, never a check. Reading getTitle() on an object that has no title yields undefined, which reaches Java as null for a String and as a meaningless value for an int. To switch between overlay types, cast() (a helper that is public final <T extends JavaScriptObject> T cast()) avoids a compiler complaint. toString() on a plain object printed [object Object]; with assertions enabled it makes a stronger effort (for example showing outerHTML of DOM nodes).

Static helpers on the base class

JavaScriptObject.createObject(), createArray(), createArray(int size) and createFunction() build empty JavaScript values; toSource() calls the object’s toSource when it has one. The JsInterop equivalents are the JsPropertyMap and Js classes of jsinterop-base (described on the JsInterop page of this section).

Wrapping a third-party JavaScript library

The recipe in four steps: load the script, write an overlay type with one native method per library function you use, hide it behind Java methods, and never pass it to code that expects Java objects.

Overlay over a library constructor, plus loading the script
package com.example.bookshelf.lab.client;

import com.google.gwt.core.client.Callback;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.ScriptInjector;

/** Overlay over a third-party BookChart constructor loaded into the host page. */
public final class ChartJso extends JavaScriptObject {
  protected ChartJso() {}

  public static native ChartJso create(double width) /*-{
    return new $wnd.BookChart(width);
  }-*/;

  public native ChartJso add(double value) /*-{
    return this.add(value);
  }-*/;

  public native String render() /*-{
    return this.render();
  }-*/;

  /** Loads the library once; the script runs in the top window so $wnd.BookChart exists. */
  public static void load(String url, Callback<Void, Exception> done) {
    ScriptInjector.fromUrl(url).setWindow(ScriptInjector.TOP_WINDOW).setCallback(done).inject();
  }
}

Points about loading:

  • ScriptInjector.fromUrl(url) returns a builder with setCallback(Callback<Void, Exception>) (called with success or the failure to load), setRemoveTag(boolean) and setWindow(JavaScriptObject); ScriptInjector.TOP_WINDOW installs the script into the host page’s window, which is what $wnd.BookChart needs. ScriptInjector.fromString(code) evaluates a script body (used in the test to define BookChart without a network). Both offer inject().

  • The traditional alternative is a <script src="chart.js"> tag in the host page before the GWT bootstrap script, so the library is ready when onModuleLoad starts. Putting the <script> in the module file (a <script src="..."/> element) is not recommended: with the xsiframe linker the compile fails when the module XML has script tags unless xsiframe.failIfScriptTag is false (see Linkers & Bootstrap).

  • The test defined BookChart with fromString(...).setWindow(ScriptInjector.TOP_WINDOW).inject() and then ChartJso.create(300).add(1).add(2.5).render() printed chart[300]:1,2.5.

  • Anything the library calls back into must be wrapped in $entry (see above), and objects the library keeps must not be Java objects that you expect to be garbage collected: a library holding a reference to a Java object keeps it alive (a leak source that the Developer Guide lists among the reasons JSNI should be used sparingly).

The "exposing a JavaScript API" half of a wrapper is the Api.install() method of the calling-Java section; deferred work (Scheduler, Timer) is described in Delayed Logic & Scheduler.

Side-by-side migration to JsInterop

JsInterop replaces JavaScript text with annotations. A native type (@JsType(isNative = true)) describes an existing JavaScript object or constructor, @JsOverlay adds Java methods to it, and an exported type (@JsType without isNative) publishes Java to JavaScript when the compiler runs with -generateJsInteropExports (the Bookshelf POM already passes it). These are the lab classes of the replacement; they compile with the same GWT version and the test produced the same output as the overlay versions.

Table 9. JSNI/overlay constructs and their JsInterop replacement
Today (JSNI or overlay) JsInterop replacement Notes

class BookJso extends JavaScriptObject

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object") class BookJs

The native type may be a plain class with @JsProperty fields; no protected-constructor/final-method rules (a native type constructor must be empty).

public final native String getTitle() /*-{ return this.title; }-*/;

@JsProperty public String title; (or a @JsProperty getter)

No JavaScript text to mistype. Names are not renamed, as with JSNI.

public String describe() { ... } on an overlay

@JsOverlay public final String describe() { ... }

Same restriction in spirit: final, Java-only, not visible to JavaScript.

JsArray<BookJso>

BookJs[] (a Java array of a native type maps to a JavaScript array)

Verified: for (BookJs b : p.books) compiled to an index loop over p.books.

return JSON.parse(text).books; in a native method

@JsMethod(namespace = JsPackage.GLOBAL, name = "JSON.parse") static native BooksPayload parse(String text);

Compiled to $wnd.JSON.parse(...).

$wnd.console.log(message)

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "console") class Console { static native void log(Object message); }

Compiled to $wnd.console.log(...); JsPackage.GLOBAL is the page’s window.

return new $wnd.BookChart(width);

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "BookChart") class Chart { Chart(double width) {} }

A native type constructor is the new.

this.add(value) wrapper method

public native Chart add(double value); on the native type

Calls are direct member calls.

$wnd.cartAdd = $entry(function(a) { ... })

@JsType(namespace = "bookshelf", name = "Cart") class CartApi with public int add(int)

Exports bookshelf.Cart (needs -generateJsInteropExports); instance methods are on the prototype.

$entry(@pkg.Class::label(...))

@JsMethod public static String label(String s, double d) in a @JsType class

Exported as bookshelf.Cart.label.

@pkg.Class::member references

plain Java calls

The compiler resolves names; no mangled signatures.

UnsafeNativeLong

use double, String or a @JsType wrapper

long is not a JavaScript value in JsInterop either; the rules are on the JsInterop page.

callbacks via $entry(function...)

@JsFunction interface

Callbacks and Promises are covered on the JsInterop page.

The JsInterop classes of the lab, side by side with the overlay of the earlier section:

BookJso replaced by a native type
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class BookJs {
  @JsProperty public int id;
  @JsProperty public String title;
  @JsProperty public String author;
  @JsProperty public int year;
  @JsProperty public int copies;

  protected BookJs() {}

  @JsOverlay
  public final String describe() {
    return title + " (" + author + ", " + year + ")";
  }

  @JsOverlay
  public final boolean inStock() {
    return copies > 0;
  }
}

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class BooksPayload {
  @JsProperty public int total;
  @JsProperty public BookJs[] books;
}

public final class Json {
  private Json() {}

  @JsMethod(namespace = JsPackage.GLOBAL, name = "JSON.parse")
  public static native BooksPayload parse(String text);
}

@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "console")
public final class Console {
  private Console() {}

  @JsMethod
  public static native void log(Object message);
}

and the code that uses them:

BooksPayload p = Json.parse(Browser.config("payload"));
for (BookJs b : p.books) {
  Console.log(b.describe());
}
new CartApi(1).add(2);

compiles to (2.13.1, -style PRETTY -generateJsInteropExports):

function $onModuleLoad(){
  var b, b$array, b$index, b$max, p, cfg;
  p = $wnd.JSON.parse((cfg = $wnd.bookshelfConfig , cfg?cfg['payload']:null));
  for (b$array = p.books , b$index = 0 , b$max = b$array.length; b$index < b$max; ++b$index) {
    b = b$array[b$index];
    $wnd.console.log(b.title + ' (' + b.author + ', ' + b.year + ')');
  }
  $add(new CartApi(1), 2);
}
...
_ = provide('bookshelf.Cart', CartApi);
_.label = label_0;

The same inlined property reads as the overlay version, with the JavaScript text gone. The exported label is installed directly (_.label = label_0); unlike the $entry wrappers of the JSNI example, the 2.13.1 output does not wrap the exported function in $entry, so do not assume that an exported JsInterop method routes its exceptions to the GWT handler; the JsInterop page of this section covers exports in detail.

Migration checklist

  1. Compile with -failOnError and fix the messages first; a migration that starts on a half-broken build is hard to review.

  2. Convert overlays first. An overlay type is the easiest to replace: each final native getter becomes a @JsProperty field or accessor, each Java-only method becomes @JsOverlay, parse helpers become @JsMethod bindings of the global (JSON.parse), and JsArray<T> becomes T[].

  3. Convert library wrappers next (new $wnd.Lib(...) becomes a native type with the library’s global name).

  4. Convert exports ($wnd.x = $entry(...)) to an exported @JsType, add -generateJsInteropExports, and test the page that calls them; the exported names are namespace.name and no longer depend on compiler renaming.

  5. Leave tiny JSNI one-liners until last; each is a single @JsMethod/@JsProperty, and many disappear when Elemental2 provides the API (see DOM & Elemental2).

  6. Re-run the tests: a GWTTestCase comparing the old and new output, as the lab did, is the cheapest regression check.

Pitfalls

Table 10. Pitfalls with exact messages and symptoms
Symptom Cause Fix

java.lang.UnsatisfiedLinkError: 'java.lang.String T.where()' on the server

A class with a JSNI method ended up on the server classpath (shared code).

Move the class to the client package, or hide it behind an interface with a client-only implementation.

Ignored 5 units with compilation errors in first pass. Compile with -strict or with -logLevel set to TRACE or DEBUG to see all errors.

Default compiler tolerance; the real JSNI errors are hidden.

Compile with -failOnError (alias -strict) or -logLevel DEBUG.

syntax error, identifier is a reserved word, illegal character, missing ; before statement

ES2015 or later syntax in a JSNI body (const, let, arrows, template literals, class).

Write ES5, or move the modern code to a loaded script or to JsInterop.

Referencing method 'pkg.C.m(I)': unable to resolve method in class 'pkg.C'

The signature after :: does not match any overload, or the method does not exist.

Fix the JNI signature (I, D, Ljava/lang/String;) or use (*) when the name is unique.

ambiguous wildcard match; both 'static ...' and 'static ...' match

(*) with overloaded methods.

Write the full signature.

Parameter 'id': type 'long' is not safe to access in JSNI code

A long in a native signature or a field reference.

double, Long, or compute in Java; @UnsafeNativeLong only to pass the value through.

A string shows up as an int, 3.7 stays 3.7, no error anywhere

Production mode does not check JSNI return types.

Test native methods; prefer JsInterop whose types the compiler knows.

Config global is undefined / null in Java

The body used window or document instead of $wnd and $doc.

Use $wnd and $doc.

Exported function throws and the GWT uncaught-exception handler never runs

The function handed to the page was not wrapped in $entry.

Wrap it: $wnd.f = $entry(function() { ... }).

that.add_0(...) is not callable from the page

Java methods are renamed; the page cannot reference them by source name.

Publish an explicit function on $wnd or export with @JsType.

'new' cannot be used to create instances of JavaScriptObject subclasses; instances must originate in JavaScript

new BookJso().

Return it from a JSNI method or a parser such as JsonUtils.safeEval.

Constructors must be 'protected' ..., Instance fields cannot be used ..., Instance methods must be 'final' ...

The overlay rules of the table above.

Make constructors protected and empty, methods final, and keep state in the JavaScript object.

instanceof BookJso is true for an unrelated object

Overlay types have no runtime type.

Validate the shape (check a property) when the source is untrusted.

Only one JavaScriptObject type may implement the methods of an interface ...

Two overlays implement the same interface.

One overlay per interface; other implementations must be ordinary Java classes.

HostedModeException ... JS value of type string, expected int

Only appears in the Developer Guide: classic Dev Mode, which no longer exists.

Nothing to do; this error does not occur in Super Dev Mode.

Outdated in older material

  • The Developer Guide JSNI page still describes classic Dev Mode (HostedModeException, "Development Mode" log window) and its advice about running in production mode to catch compile-time errors is the only behaviour left; there is no mode in 2.13.1 that checks marshalling at runtime.

  • The Manning book GWT in Action (1st edition, GWT 1.3 and 1.4) devotes chapter 8 to JSNI: the syntax, communication between Java and JavaScript, loading a JavaScript library, and wrapping a simple and a complex one. The concepts of the chapter (native methods, $wnd and $doc, the @class::member references, Java objects that are opaque in JavaScript) are all still current.

  • What changed after the book: it treats JavaScriptObject as an opaque handle and reaches the library through a separate static "Impl" class full of JSNI methods. Overlay types with real Java methods on the object (final rules, inlining, JSON overlays, JsArray, overlays implementing interfaces), $entry for callbacks coming from outside GWT, the long restriction page and the compile-time checks of this page appeared in later releases, and JsInterop (2.8) replaced the whole approach for new code.

  • The debugging material of the chapter assumes the 1.x hosted mode, and the Developer Guide’s own JSNI page still shows classic Dev Mode output; neither applies to Super Dev Mode.