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.validationandcom.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.1gwt-userjar still containscom.google.gwt.validation,javax.validationandorg.hibernate.validatormodule files. -
It speaks
javax.validation1.0. GWT’s BOM managesjavax.validation:validation-api1.0.0.GA. The client validator implements Bean Validation 1.0 and readsjavax.validation.constraintsannotations. It knows nothing aboutjakarta.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).
| 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:
-
It reads the
@GwtValidationannotation of the interface (the beans invalue, the groups ingroups). -
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. -
It writes Java source for a
_BookValidatorinterface and a_BookValidatorImplclass per bean, and a<Factory>_GwtValidatorImplclass that dispatches on the type of the object. -
The normal compiler then translates the generated Java, together with the constraint validator classes it references, to JavaScript.
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.
| Part | Role |
|---|---|
|
In the client the class is super-sourced to |
|
The only provider. Its |
|
Base class of the factory you write. You implement |
|
Annotation of the generated validator type: |
|
Base class of the generated validator; |
|
Hook for your own message keys (see Messages and locales). |
|
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-apiand itssourcesclassifier 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-apiis needed by the generator JVM: with it excluded the compiler reportedjava.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 printsSLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder"andSLF4J: Defaulting to no-operation (NOP) logger implementation, which is harmless. -
A sources jar of Hibernate Validator is mandatory. Without it the generated
_BookValidatorImpl.javafailed withNo 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 reportedRebind result 'com.google.gwt.validation.client.ProviderValidationMessageResolver' must be a classplus the same "No source code is available" lines. -
Adding Hibernate Validator annotations (
@NotEmpty,@Email,@URLand so on) to the shared bean also putshibernate-validator4.1.0.Final andslf4j-apion the compile class path of the shared module, which is another reason to keep to thejavax.validation.constraintsset 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:
| Run | Setting |
|---|---|
|
|
|
|
|
The same |
A server that bootstraps Hibernate Validator 4.1 |
Call |
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:
| Constraint | Message printed for the invalid value |
|---|---|
|
|
|
|
|
|
|
|
|
The bundle text |
|
|
|
|
|
|
|
|
|
|
Hibernate |
|
Hibernate |
|
Hibernate |
|
Hibernate |
Always fail, valid or not: |
Hibernate |
|
Rules that follow from the sources and the runs:
-
Constraints compose. The Bookshelf
@Isbnis 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 theISBN must be 13 digitsdefault declared on@Isbn, because the composed constraint was not marked@ReportAsSingleViolation. -
A custom
ConstraintValidatormust 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 usingString.formatwas rejected withThe method format(String, String) is undefined for the type Stringfollowed by anInternalCompilerExceptionthat namedChecksumValidator.classin_BookValidatorImpl.java. -
Some Hibernate constraints compile and then fail every value. The Hibernate Validator module super-sources
EmailValidator,URLValidator,ScriptAssertValidatorand theCalendarvariants of@Pastand@Futureas subclasses ofNotGwtCompatibleValidator, whoseisValidalways returnsfalse. The run confirmed it with valid data:@Emailonreader@example.comand@URLonhttps://example.com/still reportednot a well-formed email addressandmust be a valid URL, while@Length,@NotEmpty,@NotBlank,@Range,@CreditCardNumber(on4111111111111111) and@Patternaccepted valid values. Use@Patternfor an email shape on the client, or leave email checks to the server. -
Calendaris not emulated. A bean property of typejava.util.Calendarfailed the compile withNo source code is available for type java.util.Calendar; did you forget to inherit a required module?; useDate. -
Unsupported features according to the Developer Guide: XML configuration, validating non-GWT-compatible classes such as
Calendar,ConstraintValidatorFactory(validators are created withGWT.create), validation providers other than the default, and provider resolvers. In 2.13.1Validation.byProvider(...)throwsUnsupportedOperationException("GWT Validation does not support custom validator providers")andproviderResolver(...)throws the same text with "provider resolvers";AbstractGwtValidatorFactory.unwrapthrows "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,ServerChecksandUniqueIsbnValidatorare 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.classfailed withNo 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
Defaultgroup (nogroupsattribute) broke the client compile:No source code is available for type com.example.bookshelf.server.UniqueIsbnand... UniqueIsbnValidator, thenRebind 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.
| Option | How | Trade-off |
|---|---|---|
Annotate with both namespaces |
Put the |
Each rule is written twice and can drift. Measured: works, see below. Needs |
Keep a |
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 |
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 |
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) |
Source: Hibernate Validator — Releases
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);
}
}
Source: Jakarta Bean Validation
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-usercontainsjavax.validation.ConstraintViolationException_CustomFieldSerializer, a serializer for the javax exception only. Ajakarta.validation.ConstraintViolationExceptionhas no serializer. This follows from the source; the Jakarta case was not run. -
Hibernate Validator 4.1’s
ConstraintViolationImplandPathImplhave 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;
}
Source: GWT — Developer Guide: Editors
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. TheValueBoxEditorof aTextBoxreturnsnullfor empty text, so the author error is the@NotNullmessage (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@NotNulltoo. -
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.
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:
| File | Content |
|---|---|
|
Constraints in both namespaces, the |
|
The composed |
|
The server-only constraint, group and validator (not on GWT’s source path) |
|
The RPC-safe carrier of violations |
|
|
|
Custom message keys |
|
|
|
|
|
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
GWTTestCaseclass 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 apreelement) to a static host page on port 8923. Headless Chrome with a fresh--user-data-dirdumped 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
Source: GWT 2.13.1 sample — validation
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 inargLine, 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 | messagelines catches the namespace mismatch of Sharing constraints with a Jakarta Validation server immediately; theviolations=0result of the single-annotated bean is exactly what such a test would have flagged.
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
|
Hibernate Validator 4.1 reads validation XML with JAXB, which is not in JDK 11 and later |
|
The same trace on the server |
|
|
|
The module has no |
Add the rule that points to your |
|
The interface that extends |
Annotate it with the beans and groups |
|
|
List at least one bean class |
|
|
Omit |
|
The Hibernate Validator |
Add |
|
|
Add the |
|
|
Declare |
|
A server-only constraint is in a group that |
Give it its own group and leave the group out of |
|
A shared |
Rewrite it with emulated APIs, or make it a server-only constraint |
|
|
List the group in |
|
The class is not in |
Add it (and the classes of its |
The server accepts data that the browser rejects |
Server provider is in the |
Annotate both namespaces, run a |
|
Hibernate Validator 9 without an Expression Language implementation |
Add |
The server prints |
The key is missing from the server’s |
Add the key to the server bundle; pass the user’s locale to the interpolator |
An empty |
|
Put the user-facing message on |
The violations appear in a different order after each run |
|
Sort by property path before showing |
|
|
Use |
|
|
Do not call it |
|
The Hibernate Validator module replaces these validators by |
Use |
|
A constrained property has type |
Use |
Where older material is outdated
-
The Developer Guide page still shows the setup of the 2.5 era. It lists
log4j-1.2.16.jarnext to SLF4J; the measured build needed onlyslf4j-api. It says the same constraints can be reused on the server, which no longer holds for ajakarta.validationprovider without the extra work of Sharing constraints with a Jakarta Validation server. It does not mention JDK 11 orgwt.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.validationor 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-apisare optional whengwt.validation.ignoreXmlis 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).