Validation

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 (released 2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler with -sourceLevel 17, the Jakarta Servlet 6 server of the Bookshelf scenario, Hibernate Validator 4.1.0.Final for the browser side and Hibernate Validator 9.1.4.Final (Jakarta Validation 3.1) for the server side, checked on 2026-10-09. Validation is a client, shared and server topic at once. The client part is a validator that the GWT compiler generates from annotations and turns into JavaScript. The shared part is the bean class with its constraint annotations, compiled twice: by GWT to JavaScript and by javac to bytecode for the server. The server part is whatever Bean Validation provider the back end uses, which today lives in the jakarta.validation namespace and therefore does not read the annotations the GWT validator reads. The build has to supply the Hibernate Validator sources and one JVM flag. Every output block on this page was produced by running the examples in a private copy of the Bookshelf scenario: the client validator ran as GWTTestCase tests in HtmlUnit and in Super Dev Mode with headless Chrome, and the server validator ran as JUnit tests and as plain java mains with Hibernate Validator 4.1.0.Final and 9.1.4.Final. The outputs and messages are quoted as they were printed.

Client-side validation is a convenience for the user, who sees a problem before a round trip. It is never the security boundary: the server must validate the same data again, because anything that runs in a browser can be bypassed. The page is organised around that split.

The status of GWT validation

Three facts decide whether to use this module in a new project.

  • It is deprecated. The 2.8.0 release notes deprecate the JSR 303 Bean Validation support (javax.validation and com.google.gwt.validation) and give as the reason that the code has not been maintained for years. The Developer Guide page still carries a caution that the API is unmaintained and was to be moved out of the SDK into a separate project after GWT 2.8. No such project was split off: the 2.13.1 gwt-user jar still contains com.google.gwt.validation, javax.validation and org.hibernate.validator module files.

  • It speaks javax.validation 1.0. GWT’s BOM manages javax.validation:validation-api 1.0.0.GA. The client validator implements Bean Validation 1.0 and reads javax.validation.constraints annotations. It knows nothing about jakarta.validation.

  • It needs a very old provider on the build class path. The generator uses Hibernate Validator 4.1.0.Final (June 2010) as the reference implementation at compile time and compiles that library’s constraint validators to JavaScript from its source jar.

The 2.13.0 release added one thing: JAXB and xml-apis became optional for GWT’s javax.validation support, and the gwt.validation.ignoreXml system property avoids needing them. Without the property, the validator generator cannot run on JDK 11 or later (see The gwt.validation.ignoreXml flag).

Table 1. Choosing a validation strategy for a GWT 2 application
Strategy Good for Cost

Server-side Jakarta Validation only, errors returned to the browser

New code, forms where a round trip is acceptable

The user waits for the server; the editors need the violations delivered as data (see Returning violations to the browser)

GWT client validation plus the same constraints on the server (this page)

Instant feedback on a form whose rules are simple and already annotated on a shared bean

A deprecated module, an old provider, about 53 KB more JavaScript per permutation in the measured Bookshelf client, and a namespace mismatch with a Jakarta server

Hand-written checks in the presenter

A handful of rules

The rules are duplicated and can drift from the server’s rules

Prefer the first row for new work and keep the second for applications that already depend on it, as the editors page does for the display side (Editors).

The idea in one example

The Bookshelf Book is a shared class. Its constraints are ordinary Bean Validation annotations, a validator factory class tells the GWT compiler which classes to generate validators for, and one replace-with rule in the module connects the standard bootstrap to that factory.

// bookshelf-shared: compiled to JavaScript by GWT and to bytecode by javac
public class Book implements Serializable {
  @NotNull
  @Size(min = 3, max = 200, message = "{book.title.size}")
  private String title;

  @NotNull
  @Size(min = 1, message = "Author is required")
  private String author;

  @Min(value = 1450, message = "Year must be {value} or later")
  @Max(2100)
  private int year;

  @Min(0)
  private int copies;

  @Isbn(groups = {Default.class, Draft.class})
  private String isbn;

  public interface Draft {}
  // constructors, getters and setters
}
// bookshelf-client: the validator factory
public final class BookshelfValidatorFactory extends AbstractGwtValidatorFactory {

  /** Only the beans and groups listed here can be validated on the client. */
  @GwtValidation(value = Book.class, groups = {Default.class, Book.Draft.class})
  public interface GwtValidator extends Validator {}

  @Override
  public AbstractGwtValidator createValidator() {
    return GWT.create(GwtValidator.class);
  }
}
<module>
  <inherits name="com.google.gwt.user.User" />
  <inherits name="org.hibernate.validator.HibernateValidator" />

  <replace-with class="com.example.bookshelf.client.validation.BookshelfValidatorFactory">
    <when-type-is class="javax.validation.ValidatorFactory" />
  </replace-with>
  <!-- ... -->
</module>
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Book>> violations = validator.validate(book);

A book with the title ab, an empty author, the year 1200, -1 copies and the ISBN 123 produced this in the browser (HtmlUnit, sorted by property because the set has no defined order):

author | Author is required | invalid=
copies | must be greater than or equal to 0 | invalid=-1
isbn | must match "[0-9]{13}" | invalid=123
title | Title needs between 3 and 200 characters | invalid=ab
year | Year must be 1450 or later | invalid=1200

Five things happened that the rest of the page explains: the annotations were read at compile time, a validator class for Book was generated, the messages were interpolated in the browser from GWT constants, the custom key {book.title.size} was resolved by the application’s own resolver, and the numbers {min}, {max} and {value} came from the annotation attributes.

How the compiler builds the validator

Nothing is reflected at run time, as with every GWT feature. The ValidatorGenerator of the module com.google.gwt.validation.Validation is registered for every type that is assignable to javax.validation.Validator or to GwtSpecificValidator. When the compiler meets GWT.create(GwtValidator.class) it runs the generator in the compiler’s JVM:

  1. It reads the @GwtValidation annotation of the interface (the beans in value, the groups in groups).

  2. It builds a real Hibernate Validator on the JVM (the BeanHelperCache) and asks it for the metadata of each bean: which constraints are on which properties, with which attributes and groups.

  3. It writes Java source for a _BookValidator interface and a _BookValidatorImpl class per bean, and a <Factory>_GwtValidatorImpl class that dispatches on the type of the object.

  4. The normal compiler then translates the generated Java, together with the constraint validator classes it references, to JavaScript.

flowchart LR A["Book.java: constraint annotations"] --> G F["@GwtValidation interface"] --> G["ValidatorGenerator, runs in the compiler JVM"] H["Hibernate Validator 4.1.0.Final jar: reads metadata"] --> G G --> B["_BookValidatorImpl: generated Java"] G --> D["BookshelfValidatorFactory_GwtValidatorImpl: generated Java"] S["Hibernate Validator sources jar: constraint validators"] --> C B --> C["GWT compiler"] D --> C C --> J["JavaScript: no reflection, no XML"]

The generated sources are visible in the Super Dev Mode work directory, for the Bookshelf demo module target/gwt/codeserver/com.example.bookshelf.ValidationDemo/compile-2/gen/com/example/bookshelf/.... In the measured run _BookValidatorImpl.java had 695 lines for a bean with six constrained properties, and the factory implementation contained this dispatch:

public <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
  checkNotNull(object, "object");
  checkNotNull(groups, "groups");
  checkGroups(groups);
  if (object instanceof com.example.bookshelf.shared.Book) {
    // builds a GwtValidationContext and calls _BookValidator.INSTANCE.validate(...)
  }
  throw new IllegalArgumentException("BookshelfValidatorFactory.GwtValidator can not  validate "
      + object.getClass().getName() + ". " + "Valid types are [com.example.bookshelf.shared.Book]");
}

That generated code references classes such as org.hibernate.validator.constraints.impl.SizeValidatorForString. They are compiled from the sources classifier of Hibernate Validator, which is why a plain jar is not enough. HibernateValidator.gwt.xml (in gwt-user) makes the library usable: it inherits the Validation module, declares the HV constraint packages as source, super-sources GWT-compatible replacements for the validators that use unsupported APIs (EmailValidator, PatternValidator, ScriptAssertValidator and the Calendar ones among them), and registers HibernateValidationMessageResolver for the provider’s own message keys.

Table 2. Parts of the GWT validation machinery
Part Role

javax.validation.Validation

In the client the class is super-sourced to com.google.gwt.validation.client.impl.Validation. buildDefaultValidatorFactory() and byDefaultProvider() work.

GwtValidationProvider

The only provider. Its buildValidatorFactory does GWT.create(ValidatorFactory.class), which is the rule your module replaces.

AbstractGwtValidatorFactory

Base class of the factory you write. You implement createValidator(); the base class wires the message interpolator, the traversable resolver and the constraint validator factory.

@GwtValidation

Annotation of the generated validator type: value lists the bean classes, groups the groups (default Default).

AbstractGwtValidator

Base class of the generated validator; checkGroups rejects groups you did not list.

UserValidationMessagesResolver

Hook for your own message keys (see Messages and locales).

ProviderValidationMessageResolver

Hook for the provider’s keys; the Hibernate Validator module supplies it.

The published Javadoc of 2.13 does not include the validation packages (the class pages under javadoc/latest/com/google/gwt/validation/client/ answered 404 on 2026-10-09), so the source on GitHub is the reference.

Setting up the build

The Developer Guide lists the libraries that must be on the class path: the Validation API with sources (already pulled in by gwt-user), Hibernate Validator 4.1.0.Final with sources, and the SLF4J API that Hibernate Validator logs through. The Maven coordinates that compiled and ran in the Bookshelf client:

<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>4.1.0.Final</version>
</dependency>
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>4.1.0.Final</version>
  <classifier>sources</classifier>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.6.1</version>
</dependency>

What the experiments established about this list:

  • javax.validation:validation-api and its sources classifier are not needed as explicit dependencies. `gwt-user’s pom declares both, and the build succeeded without them. Declare the plain jar in the module that holds the constrained beans, because that module compiles against the annotations.

  • slf4j-api is needed by the generator JVM: with it excluded the compiler reported java.lang.ClassNotFoundException: org.slf4j.LoggerFactory. Pin 1.6.1, the version in the Developer Guide; without a pin Maven takes 1.5.6 from Hibernate Validator’s own pom. No SLF4J binding is needed. The compiler prints SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder" and SLF4J: Defaulting to no-operation (NOP) logger implementation, which is harmless.

  • A sources jar of Hibernate Validator is mandatory. Without it the generated _BookValidatorImpl.java failed with No source code is available for type org.hibernate.validator.constraints.impl.NotNullValidator; did you forget to inherit a required module? (one line per validator class).

  • The module needs <inherits name="org.hibernate.validator.HibernateValidator" />. Without it the compiler reported Rebind result 'com.google.gwt.validation.client.ProviderValidationMessageResolver' must be a class plus the same "No source code is available" lines.

  • Adding Hibernate Validator annotations (@NotEmpty, @Email, @URL and so on) to the shared bean also puts hibernate-validator 4.1.0.Final and slf4j-api on the compile class path of the shared module, which is another reason to keep to the javax.validation.constraints set unless a Hibernate constraint is needed.

The gwt.validation.ignoreXml flag

On JDK 11 and later the JAXB classes are no longer part of the JDK. Hibernate Validator 4.1.0.Final reads META-INF/validation.xml with JAXB when it bootstraps, and the GWT generator bootstraps it. Since 2.13.0 the generator honours a system property: BeanHelperCache calls config.ignoreXmlConfiguration() when System.getProperty("gwt.validation.ignoreXml") equals "true". ReflectiveServiceLayer, the server-side class of RequestFactory, reads the same property. Without the flag the Bookshelf compile on JDK 21 failed like this:

Invoking generator com.google.gwt.validation.rebind.ValidatorGenerator
   [ERROR] Unexpected error trying to instantiate Generator 'com.google.gwt.validation.rebind.ValidatorGenerator'
java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException
	at org.hibernate.validator.engine.ConfigurationImpl.parseValidationXml(ConfigurationImpl.java:252)
	at org.hibernate.validator.engine.ConfigurationImpl.buildValidatorFactory(ConfigurationImpl.java:143)
	at com.google.gwt.validation.rebind.BeanHelperCache.<init>(BeanHelperCache.java:60)
	at com.google.gwt.validation.rebind.ValidatorGenerator.<init>(ValidatorGenerator.java:43)
...
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXBException
   [ERROR] Errors in 'com/example/bookshelf/client/validation/BookshelfValidatorFactory.java'
      [ERROR] Line 20: Failed to resolve 'com.example.bookshelf.client.validation.BookshelfValidatorFactory.GwtValidator' via deferred binding

The property must reach every JVM that runs the generator. The place differs per Maven goal, and two of them are easy to get wrong:

Table 3. Where -Dgwt.validation.ignoreXml=true goes
Run Setting

gwt:compile

<jvmArgs><arg>-Dgwt.validation.ignoreXml=true</arg></jvmArgs> in the plugin configuration (the parameter is documented as "Arguments to be passed to the forked JVM").

gwt:test and Surefire

<argLine>-Dgwt.validation.ignoreXml=true</argLine>, or -DargLine=-Dgwt.validation.ignoreXml=true on the command line. The GWTTestCase compile runs inside the test JVM.

gwt:codeserver (Super Dev Mode)

The same jvmArgs element, but in the plugin configuration of the root pom. The goal runs on the aggregator project: with jvmArgs only in bookshelf-client/pom.xml the Super Dev Mode recompile failed with the JAXBException trace above, and with the element in the root pom the same module compiled.

A server that bootstraps Hibernate Validator 4.1

Call ignoreXmlConfiguration() on the configuration: Validation.byDefaultProvider().configure().ignoreXmlConfiguration().buildValidatorFactory(). On JDK 21 Validation.buildDefaultValidatorFactory() threw java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException for the server too.

The alternative is to put javax.xml.bind:jaxb-api and a JAXB implementation on the compiler class path, which is what the flag was added to avoid. The browser code never needs JAXB: BaseGwtConfiguration.ignoreXmlConfiguration() in the client is a documented no-op ("Always ignore XML anyway"), so XML constraint mappings are not available in GWT.

Writing constraints for the client

The generator accepts the constraints of Bean Validation 1.0 whose validators are GWT-compatible. The Bookshelf run put one instance of each on a bean (invalid values everywhere) and the client reported all of them:

Table 4. Constraints validated in the browser, with the messages printed
Constraint Message printed for the invalid value

@NotNull

may not be null

@Null

must be null

@AssertTrue, @AssertFalse

must be true, must be false

@Min

must be greater than or equal to 0

@Max

The bundle text must be less than or equal to followed by the value (not violated in the run)

@DecimalMin, @DecimalMax

must be greater than or equal to 1.5, must be less than or equal to 1.5

@Digits

numeric value out of bounds (<2 digits>.<1 digits> expected)

@Size on a String, List or Map

size must be between 0 and 1

@Past, @Future on a Date

must be in the past, must be in the future

@Pattern

must match "[a-z]+" (the regular expression is evaluated by GWT’s RegExp, so it is a JavaScript regular expression)

Hibernate @NotEmpty, @NotBlank

may not be empty

Hibernate @Length

length must be between 3 and 2147483647

Hibernate @Range

must be between 1 and 5

Hibernate @Email, @URL

Always fail, valid or not: not a well-formed email address, must be a valid URL

Hibernate @CreditCardNumber

invalid credit card number

Rules that follow from the sources and the runs:

  • Constraints compose. The Bookshelf @Isbn is declared as @Pattern(regexp = "[0-9]{13}") plus @Constraint(validatedBy = {}), so it needs no validator class. The client reported the message of the composing @Pattern (must match "[0-9]{13}"), not the ISBN must be 13 digits default declared on @Isbn, because the composed constraint was not marked @ReportAsSingleViolation.

  • A custom ConstraintValidator must be GWT-compatible and shared. It is compiled to JavaScript like any other class, so it must stay within the JRE emulation (see JRE emulation and shared code). A validator using String.format was rejected with The method format(String, String) is undefined for the type String followed by an InternalCompilerException that named ChecksumValidator.class in _BookValidatorImpl.java.

  • Some Hibernate constraints compile and then fail every value. The Hibernate Validator module super-sources EmailValidator, URLValidator, ScriptAssertValidator and the Calendar variants of @Past and @Future as subclasses of NotGwtCompatibleValidator, whose isValid always returns false. The run confirmed it with valid data: @Email on reader@example.com and @URL on https://example.com/ still reported not a well-formed email address and must be a valid URL, while @Length, @NotEmpty, @NotBlank, @Range, @CreditCardNumber (on 4111111111111111) and @Pattern accepted valid values. Use @Pattern for an email shape on the client, or leave email checks to the server.

  • Calendar is not emulated. A bean property of type java.util.Calendar failed the compile with No source code is available for type java.util.Calendar; did you forget to inherit a required module?; use Date.

  • Unsupported features according to the Developer Guide: XML configuration, validating non-GWT-compatible classes such as Calendar, ConstraintValidatorFactory (validators are created with GWT.create), validation providers other than the default, and provider resolvers. In 2.13.1 Validation.byProvider(...) throws UnsupportedOperationException("GWT Validation does not support custom validator providers") and providerResolver(...) throws the same text with "provider resolvers"; AbstractGwtValidatorFactory.unwrap throws "GWT Validation does not support upwrap()".

  • Property paths are the bean property names (title, author.name), the strings the Editor framework uses (see Integration with the Editor framework).

Creating a validator and validating

Validation.buildDefaultValidatorFactory().getValidator() returns the generated validator. Build it once and keep it: getValidator() calls createValidator() and initialises a new object each time. The validator offers the three standard methods, all measured:

Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

validator.validate(book);                                // every property, Default group
validator.validateProperty(book, "title");               // one property of an instance
validator.validateValue(Book.class, "year", 1000);       // one property, any value, no instance
validateProperty title: [title | Title needs between 3 and 200 characters]
validateValue year=1000: [year | Year must be 1450 or later]

validateValue is the one to call from a ValueChangeHandler for as-you-type feedback, since it needs no bean. A null property is a violation of @NotNull: for a book with a null title and author the printed output was:

author | may not be null | invalid=null
title | may not be null | invalid=null

validate returns a HashSet, so the order of the violations is not stable (the Super Dev Mode run printed isbn, year, author, title, copies). Sort them, or key them by property path, before showing them.

getConstraintsForClass(Book.class).isBeanConstrained() returned true, and Validation.buildDefaultValidatorFactory().getClass().getName() was com.example.bookshelf.client.validation.BookshelfValidatorFactory, which confirms that the module’s replace-with rule is the one in force.

Groups: choosing which constraints run where

A group is an empty interface. Every constraint belongs to Default unless it names groups, and the validator checks only the groups it was generated for. The @GwtValidation annotation must list every group the client will use:

@GwtValidation(value = Book.class, groups = {Default.class, Book.Draft.class})
public interface GwtValidator extends Validator {}

The Bookshelf @Isbn constraint is in Default and in Draft, so the "save as draft" screen can check only the ISBN:

Draft group: [isbn | must match "[0-9]{13}" | invalid=123]
Default+Draft: 2
Default: 2

The three lines are validate(book, Book.Draft.class), validate(book, Default.class, Book.Draft.class).size() and validate(book, Default.class).size(): the book has a too short title and a bad ISBN, so Default alone already finds two (the title and the ISBN, which is also in Default), and adding Draft finds the same two.

Asking for a group that was not listed fails at run time, not at compile time:

IllegalArgumentException: class com.example.bookshelf.client.validation.BookshelfValidatorFactory_GwtValidatorImpl only processes the following groups [interface javax.validation.groups.Default, interface com.example.bookshelf.shared.Book$Draft]. The following groups could not be processed [interface com.example.bookshelf.client.ValidationGwtTest$Unlisted]

Validating an object whose class is not in value fails the same way:

IllegalArgumentException: BookshelfValidatorFactory.GwtValidator can not  validate java.lang.Object. Valid types are [com.example.bookshelf.shared.Book]

The upstream sample lists only Person in value although Person has @Valid associations to Address objects, so a bean reached through @Valid is validated as part of its owner and does not need its own entry.

Constraints that only the server can evaluate

Groups are also the way to keep a server-only rule on a shared bean. The Developer Guide’s best-practice section says to omit server-only groups from @GwtValidation "to avoid compilation issues". The Bookshelf rule "no other book may have the same ISBN" needs the repository, so it is a server constraint in a server group:

// package com.example.bookshelf.server: in the shared jar, but not on GWT's source path
public interface ServerChecks {}

@Constraint(validatedBy = {UniqueIsbnValidator.class})
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueIsbn {
  String message() default "ISBN already in the catalogue";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
}

// on Book
@UniqueIsbn(groups = ServerChecks.class)
private String isbn;

Three consequences were measured:

  • The client compiled although UniqueIsbn, ServerChecks and UniqueIsbnValidator are not on the module’s source path, because the generator never touches constraints in groups it was not asked for.

  • Client code cannot name the server group. A test that referred to ServerChecks.class failed with No source code is available for type com.example.bookshelf.server.ServerChecks; did you forget to inherit a required module?.

  • Putting the same constraint in the Default group (no groups attribute) broke the client compile: No source code is available for type com.example.bookshelf.server.UniqueIsbn and ... UniqueIsbnValidator, then Rebind result 'com.example.bookshelf.shared._BookValidatorImpl' could not be found.

Messages and locales

A violation message is a template, interpolated in the browser by GwtMessageInterpolator. The algorithm of the Bean Validation specification runs in this order: replace {key} parameters using the user resolver until nothing changes, then using the provider resolver, repeat while the second step changed something, then replace the annotation’s attributes ({min}, {max}, {value}), and finally remove the escapes. The provider resolver is HibernateValidationMessageResolver, which serves the keys of org.hibernate.validator.ValidationMessages (such as javax.validation.constraints.NotNull.message); the module ships English, German, French, Turkish and Mongolian files, and the GWT locale property selects among them. That is why the violations of the test module, which is fixed to the en locale, are English.

Your own keys need a resolver. A ConstantsWithLookup interface holds the messages, a class that extends AbstractValidationMessageResolver and implements UserValidationMessagesResolver wraps it, and the module replaces the default (NullUserValidationMessageResolver, which returns nothing):

public interface ValidationMessages extends ConstantsWithLookup {
  @Key("book.title.size")
  @DefaultStringValue("Title needs between {min} and {max} characters")
  String book_title_size();
}

public class BookshelfMessagesResolver extends AbstractValidationMessageResolver
    implements UserValidationMessagesResolver {
  protected BookshelfMessagesResolver() {
    super((ConstantsWithLookup) GWT.create(ValidationMessages.class));
  }
}
<replace-with class="com.example.bookshelf.client.validation.BookshelfMessagesResolver">
  <when-type-is class="com.google.gwt.validation.client.UserValidationMessagesResolver" />
</replace-with>

AbstractValidationMessageResolver.get replaces each . in the key by _ before the lookup, so book.title.size is read as the method book_title_size. Locale variants are the usual GWT ones: ValidationMessages_es.properties next to the interface, and a compiled es locale (see Internationalization). The constraint says message = "{book.title.size}", and the browser printed Title needs between 3 and 200 characters, with {min} and {max} taken from the annotation.

The server needs the same key or it prints the key itself. Hibernate Validator on the server resolves {book.title.size} from a ValidationMessages.properties at the root of its class path (the Bookshelf server has one with the identical line), and it chooses the language from the JVM default locale, not from the browser:

copies | must be greater than or equal to 0
copies | debe ser mayor que o igual a 0

The first line was printed with the JVM default locale en, the second on the same machine without any code change when the default locale was Spanish.

Pass the user’s locale explicitly (a custom MessageInterpolator or a locale-aware LocaleResolver) if the server’s messages must match the language of the page.

Sharing constraints with a Jakarta Validation server

This is the part that changed most since the Developer Guide was written. The guide says that the same constraints can be reused for server-side validation. That is true when the server speaks javax.validation, as Hibernate Validator 4.x, 5.x and 6.2 do. The Bookshelf server runs on Jakarta Servlet 6, and the maintained Bean Validation providers for that platform (Hibernate Validator 7.0 and later; 8.0 for Jakarta EE 10, 9.x for Jakarta EE 11) use the namespace jakarta.validation. Their annotation types are different classes with the same simple names.

The experiment used the Bookshelf Book with javax.validation annotations only, compiled once and validated by Hibernate Validator 9.1.4.Final (Jakarta Validation 3.1.1, Expressly 6.0.0 as the expression language):

violations=0

The same invalid book that produced five violations in the browser produced none on the server. Hibernate Validator 9 does not read javax.validation.constraints annotations and does not report that it ignored them. This is the most dangerous failure mode of the whole page, because everything compiles, the client works, and the server silently accepts what the browser rejected.

Table 5. Ways to share one bean between GWT 2 and a Jakarta server
Option How Trade-off

Annotate with both namespaces

Put the javax.validation.constraints annotation (GWT, Hibernate Validator 4.1) and its jakarta.validation.constraints twin on the same property

Each rule is written twice and can drift. Measured: works, see below. Needs jakarta.validation-api on the shared module’s compile path only

Keep a javax server

Run a provider in the old namespace, for example Hibernate Validator 6.2 (the release page says "Keep javax.* packages"; limited support), on a servlet container that the javax namespace still serves

Pins the server to old dependencies; not run on this page

Validate on the server with a separate Jakarta model

Keep the rules on the server in Jakarta annotations or Hibernate’s programmatic mapping and let the shared bean carry only the javax annotations used by the browser

The two rule sets are in different places and need tests that compare them; not run on this page

Do not validate in the browser

Only the server validates; the browser displays the returned violations

The recommended path for new code (see Returning violations to the browser)

Annotating both namespaces

GWT’s generator reads only the javax.validation metadata and ignored the Jakarta annotations in the run (the Jakarta API jar was on the compile class path). Adding a jakarta.validation.constraints.NotNull to a property next to the javax one did not change the generated validator: the test that validates a valid book still passed, and the client output above is unchanged with every Bookshelf property dual-annotated. The Bookshelf property looks like this:

@NotNull
@Size(min = 3, max = 200, message = "{book.title.size}")
@jakarta.validation.constraints.NotNull
@jakarta.validation.constraints.Size(min = 3, max = 200, message = "{book.title.size}")
private String title;

@Isbn(groups = {Default.class, Draft.class})
@jakarta.validation.constraints.Pattern(regexp = "[0-9]{13}", message = "ISBN must be 13 digits",
    groups = {jakarta.validation.groups.Default.class, Draft.class})
@UniqueIsbn(groups = ServerChecks.class)   // a jakarta.validation constraint: server only
private String isbn;

The shared module declares javax.validation:validation-api and jakarta.validation:jakarta.validation-api:3.1.1; the Jakarta groups use jakarta.validation.groups.Default, a different type from the javax one. Hibernate Validator 9.1.4.Final then printed the five violations of the client (with the JVM locale forced to English):

violations=5
  author | Author is required | invalid=
  copies | must be greater than or equal to 0 | invalid=-1
  isbn | ISBN must be 13 digits | invalid=123
  title | Title needs between 3 and 200 characters | invalid=ab
  year | Year must be 1450 or later | invalid=1200

The isbn message differs on purpose in this example: the browser reports the message of the composed @Isbn (the @Pattern text) and the server the text of the Jakarta @Pattern. Keep messages identical, or use the same {key} on both sides, when users should not notice which side rejected the data.

The server-only @UniqueIsbn is a Jakarta constraint from the start, because only the server evaluates it. It is not visible to GWT at all (its package is not on the source path and its group is not listed).

Hibernate Validator 9 needs an Expression Language implementation. With only the API on the class path the first buildDefaultValidatorFactory() failed with jakarta.validation.ValidationException: HV000183: Unable to initialize 'jakarta.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead. Add org.glassfish.expressly:expressly:6.0.0 (the Bookshelf server did), or use Jakarta’s ParameterMessageInterpolator.

The server validates again

The Bookshelf CatalogServiceImpl.saveBook used to check the title by hand; it now validates with the shared constraints plus the server group, and sends the violations back as plain data:

public class CatalogServiceImpl extends XsrfProtectedServiceServlet implements CatalogService {
  /** Thread-safe and expensive to build, so created once. The server is authoritative: it validates again. */
  private static final Validator VALIDATOR =
      Validation.buildDefaultValidatorFactory().getValidator();   // jakarta.validation

  @Override
  public Book saveBook(Book book) throws BookNotFoundException, InvalidBookException {
    Set<ConstraintViolation<Book>> violations =
        VALIDATOR.validate(book, Default.class, ServerChecks.class);
    if (!violations.isEmpty()) {
      List<ViolationDto> dtos = new ArrayList<>();
      for (ConstraintViolation<Book> v : violations) {
        dtos.add(new ViolationDto(v.getPropertyPath().toString(), v.getMessage()));
      }
      throw new InvalidBookException(dtos);
    }
    return CatalogRepository.INSTANCE.save(book);
  }
}

The JUnit tests of the server ran in 0.1 s each and printed:

VIOLATION author | Author is required
VIOLATION copies | must be greater than or equal to 0
VIOLATION isbn | ISBN must be 13 digits
VIOLATION title | Title needs between 3 and 200 characters
VIOLATION year | Year must be 1450 or later
VIOLATION isbn | ISBN already in the catalogue

The first five lines come from the first test; the last line comes from the second test, which registers an ISBN and then saves a book with the same one, so only the server-only group objects. A third test saved a valid book. The first two are the useful ones: they call the service class directly, so they prove the server’s rules without a browser, which is how the server should be tested.

Returning violations to the browser

How violations travel back depends on the transport. For GWT-RPC the safe design, and the one used above, is a small serializable exception that carries the property path and the message (InvalidBookException with a list of ViolationDto), declared in the service interface. Three facts from the sources explain why the Developer Guide’s ConstraintViolationException approach does not carry over to a Jakarta server:

  • gwt-user contains javax.validation.ConstraintViolationException_CustomFieldSerializer, a serializer for the javax exception only. A jakarta.validation.ConstraintViolationException has no serializer. This follows from the source; the Jakarta case was not run.

  • Hibernate Validator 4.1’s ConstraintViolationImpl and PathImpl have GWT custom field serializers (org.hibernate.validator.engine), which is how the Developer Guide’s sample returns violations. They belong to the old provider.

  • A violation holds a root bean and metadata that should not travel to a browser anyway.

On the client, turn the DTOs back into editor errors. The editors page shows a minimal ConstraintViolation implementation (ServerViolation) that carries the root bean, the path and the message, and explains why the root bean must be the object returned by flush() (Editors). GWT-RPC itself, its serialization policy and its security are the subject of GWT-RPC and XSRF, RPC & Server Security. For the validation rules of the server platform in general, see Bean Validation in Jakarta Faces and Quarkus validation.

Integration with the Editor framework

The Editor framework does not validate; it displays errors that something else computed. EditorDriver.setConstraintViolations(Iterable<ConstraintViolation<?>>) converts each violation into an EditorError at the editor whose path equals the violation’s property path and whose root bean is the instance the driver edited. The workflow of the Bookshelf BookEditor:

private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

/** Flushes the form, validates the same Book instance and shows the violations on the editors. */
public boolean trySave() {
  Book book = driver.flush();
  if (driver.hasErrors()) {
    return false;                       // a widget could not parse its input
  }
  Set<ConstraintViolation<Book>> violations = validator.validate(book);
  if (!violations.isEmpty()) {
    driver.setConstraintViolations(new HashSet<ConstraintViolation<?>>(violations));
    return false;
  }
  listener.onSave(book);
  return true;
}

A book with the title ab, an empty author field and the year 1200 gave:

saved=0 hasErrors=true
errors=[author -> may not be null, title -> Title needs between 3 and 200 characters, year -> Year must be 1450 or later]
after fix saved=1

trySave() returned false with the invalid book (nothing was passed to the listener) and true after the fields were corrected and the book was edited again.

Two details:

  • An empty text box flushes as null, not as an empty string. The ValueBoxEditor of a TextBox returns null for empty text, so the author error is the @NotNull message (may not be null), not the @Size(min = 1, message = "Author is required") message that the same value produced when validated directly on a bean with "". If the form should say "Author is required" for an empty box, put the message on @NotNull too.

  • Only violations the driver can place are shown. A violation on a property for which the form has no editor has nowhere to go; the Editors page lists the other rules (the argument type, the root-bean identity, the return value of setConstraintViolations).

The same page covers ValueBoxEditorDecorator, which shows an error label next to the field, and the RequestFactory driver, whose server side runs a JSR-303 validator of its own: ReflectiveServiceLayer builds Validation.byDefaultProvider().configure() and honours gwt.validation.ignoreXml too.

sequenceDiagram participant U as User participant E as BookEditor participant V as Browser validator participant R as RPC stub participant S as CatalogServiceImpl participant J as Jakarta validator U->>E: presses Save E->>E: driver.flush() returns the Book E->>V: validate(book) alt client violations V-->>E: violations E-->>U: driver.setConstraintViolations shows the errors else no client violations E->>R: saveBook(book) R->>S: HTTP POST (XSRF token checked) S->>J: validate(book, Default, ServerChecks) alt server violations J-->>S: violations S-->>R: InvalidBookException with ViolationDto list R-->>E: onFailure(InvalidBookException) E-->>U: the violations as editor errors else valid S-->>R: saved Book R-->>E: onSuccess end end

The Bookshelf worked example

The scratch application is the three-module Bookshelf (bookshelf-shared, bookshelf-client, bookshelf-server) from the project structure page (Project structure and modules). The pieces added for this page:

Table 6. Files of the validation example
File Content

bookshelf-shared/.../shared/Book.java

Constraints in both namespaces, the Draft group

bookshelf-shared/.../shared/Isbn.java

The composed @Isbn constraint (@Pattern)

bookshelf-shared/.../server/UniqueIsbn.java, ServerChecks.java, UniqueIsbnValidator.java

The server-only constraint, group and validator (not on GWT’s source path)

bookshelf-shared/.../shared/InvalidBookException.java, ViolationDto.java

The RPC-safe carrier of violations

bookshelf-client/.../validation/BookshelfValidatorFactory.java

AbstractGwtValidatorFactory with @GwtValidation

bookshelf-client/.../validation/ValidationMessages.java, BookshelfMessagesResolver.java

Custom message keys

bookshelf-client/src/main/module.gwt.xml

HibernateValidator inherit and two replace-with rules

bookshelf-server/.../CatalogServiceImpl.java

jakarta.validation on saveBook

bookshelf-server/src/main/resources/ValidationMessages.properties

The same message keys for the server

The commands that were run (JDK 21, offline Maven after the first resolution):

# client validator tests (GWTTestCase in HtmlUnit); the flag reaches the test JVM through argLine
mvn -B verify -pl bookshelf-client -am -Dtest=ValidationGwtTest -Dsurefire.failIfNoSpecifiedTests=false

# server tests
mvn -B test -pl bookshelf-server -am -Dtest=CatalogServiceValidationTest -Dsurefire.failIfNoSpecifiedTests=false

# production compile with the flag in the plugin's jvmArgs
mvn -B package -pl bookshelf-client -am -DskipTests

# Super Dev Mode on port 9923 (codeserverArgs -port 9923 and jvmArgs in the root pom)
mvn -B gwt:codeserver -pl bookshelf-client -am -Dgwt.modules=com.example.bookshelf.ValidationDemo -DlauncherDir=...

Results:

  • The eight tests of the GWTTestCase class took about 4 to 6 seconds in total, including the compile of the test module; the three server tests passed in about 0.1 s.

  • The production compile of the client (two locales, six permutations) finished with Compilation succeeded — 11,593s, and the per-permutation JavaScript grew from about 264 KB (263,127 to 264,368 bytes) in the Bookshelf client without a validator to about 317 KB (316,501 to 317,857 bytes) with it. That is about 53 KB for one validated bean and the Hibernate Validator constraint code, before gzip, and it is the main cost of the module. The numbers come from the same client before and after the validator was added; the editor form changed with it (trySave).

  • Super Dev Mode on port 9923 served a one-page demo module (ValidationDemo, an entry point that validates the invalid book and writes the violations into a pre element) to a static host page on port 8923. Headless Chrome with a fresh --user-data-dir dumped the DOM after the recompile:

violations:
isbn | must match "[0-9]{13}"
year | Year must be 1450 or later
author | Author is required
title | Title needs between 3 and 200 characters
copies | must be greater than or equal to 0

The upstream samples/validation project of the GWT repository (client, shared and server modules generated from the modular-webapp archetype) is the closest official reference: it uses a ServerConstraint in a server group, a custom messages resolver, Validation.byDefaultProvider().configure().ignoreXmlConfiguration() on the server, and throws ConstraintViolationException from an RPC method (all with the javax namespace).

Testing validation

  • Test the server rules with plain JUnit against the service class, as above: no browser and no GWT.

  • Test the client rules with GWTTestCase (see Testing). The tests need the flag in argLine, and the console output of HtmlUnit is not captured, so assert on values rather than printing them.

  • Compare the two sides with the same invalid fixtures. A test that validates the same book in both JVMs and compares the sorted path | message lines catches the namespace mismatch of Sharing constraints with a Jakarta Validation server immediately; the violations=0 result of the single-annotated bean is exactly what such a test would have flagged.

Pitfalls

Table 7. Common problems with GWT validation
Symptom Cause Fix

[ERROR] Unexpected error trying to instantiate Generator 'com.google.gwt.validation.rebind.ValidatorGenerator' with java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException and Failed to resolve '...GwtValidator' via deferred binding

Hibernate Validator 4.1 reads validation XML with JAXB, which is not in JDK 11 and later

-Dgwt.validation.ignoreXml=true in jvmArgs (compile), argLine (tests) and the root pom’s jvmArgs (gwt:codeserver)

The same trace on the server

Validation.buildDefaultValidatorFactory() with Hibernate Validator 4.1 on JDK 21

Validation.byDefaultProvider().configure().ignoreXmlConfiguration().buildValidatorFactory()

Rebind result 'javax.validation.ValidatorFactory' must be a class in GwtValidationProvider.java

The module has no replace-with for javax.validation.ValidatorFactory

Add the rule that points to your AbstractGwtValidatorFactory subclass

BookshelfValidatorFactory.GwtValidator must be anntotated with com.google.gwt.validation.client.GwtValidation (the misspelling is in the message)

The interface that extends Validator has no @GwtValidation

Annotate it with the beans and groups

The @GwtValidation of BookshelfValidatorFactory.GwtValidatormust specify at least one bean type to validate.

@GwtValidation({})

List at least one bean class

The @GwtValidation of BookshelfValidatorFactory.GwtValidatormust specify at least one validation group.

groups = {}

Omit groups for Default or list the groups

No source code is available for type org.hibernate.validator.constraints.impl.NotNullValidator; did you forget to inherit a required module? (and Min, Max, Size validators)

The Hibernate Validator sources jar is missing from the class path

Add hibernate-validator:4.1.0.Final:sources

Rebind result 'com.google.gwt.validation.client.ProviderValidationMessageResolver' must be a class

org.hibernate.validator.HibernateValidator is not inherited

Add the <inherits> line

java.lang.ClassNotFoundException: org.slf4j.LoggerFactory in the generator

slf4j-api is missing from the compile class path

Declare org.slf4j:slf4j-api:1.6.1

No source code is available for type com.example.bookshelf.server.UniqueIsbn (or the group class) in a generated _BookValidatorImpl.java

A server-only constraint is in a group that @GwtValidation lists, including Default

Give it its own group and leave the group out of @GwtValidation

The method format(String, String) is undefined for the type String and an InternalCompilerException

A shared ConstraintValidator uses an API outside the JRE emulation

Rewrite it with emulated APIs, or make it a server-only constraint

IllegalArgumentException: ... only processes the following groups [...]. The following groups could not be processed [...]

validate(bean, SomeGroup.class) with a group missing from groups

List the group in @GwtValidation

IllegalArgumentException: BookshelfValidatorFactory.GwtValidator can not validate java.lang.Object. Valid types are [...]

The class is not in @GwtValidation.value

Add it (and the classes of its @Valid associations)

The server accepts data that the browser rejects

Server provider is in the jakarta.validation namespace and the bean only has javax.validation annotations: 0 violations measured

Annotate both namespaces, run a javax provider, or move the rules into server-side mappings

HV000183: Unable to initialize 'jakarta.el.ExpressionFactory' on the server

Hibernate Validator 9 without an Expression Language implementation

Add org.glassfish.expressly:expressly:6.0.0 or use ParameterMessageInterpolator

The server prints {book.title.size} or another language than the page

The key is missing from the server’s ValidationMessages.properties; the server uses the JVM default locale

Add the key to the server bundle; pass the user’s locale to the interpolator

An empty TextBox is reported as may not be null, not by @Size(min = 1)

ValueBoxEditor flushes empty text as null

Put the user-facing message on @NotNull

The violations appear in a different order after each run

validate returns a HashSet

Sort by property path before showing

UnsupportedOperationException: GWT Validation does not support custom validator providers

Validation.byProvider(...) in client code

Use Validation.buildDefaultValidatorFactory() or byDefaultProvider()

GWT Validation does not support upwrap()

ValidatorFactory.unwrap in client code

Do not call it

@Email or @URL reports an error for a valid value

The Hibernate Validator module replaces these validators by NotGwtCompatibleValidator, which always fails

Use @Pattern on the client and check on the server

No source code is available for type java.util.Calendar; did you forget to inherit a required module?

A constrained property has type Calendar, which is not emulated

Use Date

Where older material is outdated

  • The Developer Guide page still shows the setup of the 2.5 era. It lists log4j-1.2.16.jar next to SLF4J; the measured build needed only slf4j-api. It says the same constraints can be reused on the server, which no longer holds for a jakarta.validation provider without the extra work of Sharing constraints with a Jakarta Validation server. It does not mention JDK 11 or gwt.validation.ignoreXml.

  • The 2.5.0 release notes announced that validation support was improved, documented and no longer experimental; the 2.8.0 notes deprecated it.

  • GWT in Action (Manning, 2007) was written before JSR-303 was final; the extracted text of the book does not mention JSR-303, javax.validation or Hibernate Validator. It is useful for how widgets report input errors, not for validation frameworks.

What changed recently

  • 2.5.0 (RC2 notes): validation documented and no longer experimental.

  • 2.8.0 (RC1 notes): Bean Validation support deprecated, with a plan to move it out of the SDK that was not carried out.

  • 2.13.0 (2026-02-11): JAXB and xml-apis are optional when gwt.validation.ignoreXml is set.

  • Hibernate Validator 9.1.4.Final (2026-09-20) is the latest series on Hibernate’s release page; 8.0 and 6.2 are in limited support. Jakarta Validation 3.1.1 is the current stable specification API on Maven Central (a 4.0.0-M1 milestone also exists).