Table of contents
Open Table of contents
Learning Recommendations
I recommend studying Hibernate Validator alongside the Jakarta Bean Validation specification.
If you forget how to use an annotation or feature, the official Hibernate Validator examples provide a quick reference.
The example code is clearly organized, with a directory for each chapter.

All Constraint Annotations
The Jakarta Bean Validation 3.0 specification lists all built-in annotations and their constraints.
Notes on @NotEmpty and @NotBlank
- Both check for null, so adding @NotNull is unnecessary.
Notes on @Size
- This annotation applies to strings, collections, arrays, and Maps. Use @Max and @Min to validate numeric values.
Where Constraint Annotations Can Be Placed
JavaBean
-
public class Car { @NotNull private String manufacturer; @AssertTrue private boolean isRegistered; public Car(String manufacturer, boolean isRegistered) { this.manufacturer = manufacturer; this.isRegistered = isRegistered; } //getters and setters... } -
Property-level constraints have the same effect as the approach above. Put them on getters, not setters.
The property’s getter method has to be annotated, not its setter. That way also read-only properties can be constrained which have no setter method
public class Car { private String manufacturer; private boolean isRegistered; public Car(String manufacturer, boolean isRegistered) { this.manufacturer = manufacturer; this.isRegistered = isRegistered; } @NotNull public String getManufacturer() { return manufacturer; } public void setManufacturer(String manufacturer) { this.manufacturer = manufacturer; } @AssertTrue public boolean isRegistered() { return isRegistered; } public void setRegistered(boolean isRegistered) { this.isRegistered = isRegistered; } } -
Container-element constraints support List, Set, Map, and Optional.
-
Object graphs: when a JavaBean property is another JavaBean, add @Valid to the bean to be validated for cascading validation.
public class Car { private List<@NotNull @Valid Person> passengers = new ArrayList<Person>(); }This alternative also works, although the official documentation does not recommend it.
public class Car { @Valid private List<@NotNull Person> passengers = new ArrayList<Person>(); }In versions prior to 6, Hibernate Validator supported cascaded validation for a subset of container elements and it was implemented at the container level (e.g. you would use @Valid private List
to enable cascaded validation for Person). This is still supported but is not recommended. Please use container element level @Valid annotations instead as it is more expressive.
-
Class-level constraints place the annotation on the class rather than an individual property.
The Validator Interface
Validator handles the JavaBean and property validation above. It provides these methods:
Validator#validate()
Validator#validateProperty()
Validator#validateValue()
For usage, see the official examples or the Validator examples in the documentation.
Constraint Inheritance
Constraint annotations are inherited. Annotations placed on a superclass or interface are automatically inherited by subclasses and implementations.
When a class implements an interface or extends another class, all constraint annotations declared on the super-type apply in the same manner as the constraints specified on the class itself
Parameters and Return Values of Methods and Constructors
- Place constraint annotations on method parameters to validate the inputs.
- Place constraint annotations on a method to validate its return value.
- Place constraint annotations on constructor parameters to validate the inputs.
- Place constraint annotations on a constructor to validate its return value.
For example:
public class RentalStation {
public RentalStation(@NotNull String name) {
}
public void rentCar(
@NotNull Customer customer,
@NotNull @Future Date startDate,
@Min(1) int durationInDays) {
}
}
public class RentalStation {
@ValidRentalStation
public RentalStation() {
}
@NotNull
@Size(min = 1)
public List<@NotNull Customer> getCustomers() {
return null;
}
}
The ExecutableValidator Interface
ExecutableValidator validates parameters and return values of methods and constructors. It provides these methods:
validateParameters()
validateReturnValue()
validateConstructorParameters()
validateConstructorReturnValue()
For usage, see the official examples or the ExecutableValidator examples in the documentation.
Constraint Inheritance
- For parameter validation, a subclass cannot override constraints already declared in the superclass. For example:
public interface Vehicle { void drive(@Max(75) int speedInMph); } public class Car implements Vehicle { @Override public void drive(@Max(55) int speedInMph) { //... } } - For parameter validation, if a method overrides or implements methods declared in multiple parallel supertypes—for example, the same method in two interfaces implemented by a class—parameter constraints cannot be specified on any of the involved methods. For example:
public interface Vehicle { void drive(@Max(75) int speedInMph); } public interface Car { void drive(int speedInMph); } public class RacingCar implements Car, Vehicle { @Override public void drive(int speedInMph) { //... } } - For return-value validation, constraints in the superclass can be overridden.
See the official documentation for a detailed explanation.
Internationalized Error Messages
Each annotation has a message attribute specifying the error message used when validation fails. If internationalization is unnecessary, simply put the error message directly in this attribute.
If the engineering team spans several countries, internationalized error logs make troubleshooting easier for engineers in different regions.
Java developers with web experience may know ResourceBundle, the class responsible for internationalization. Here is a common, simple example:
// In practice, obtain language and country from the Accept-Language request header
Locale locale = new Locale(language, country);
// Or use the default (https://stackoverflow.com/questions/24305512/how-to-get-the-default-resourcebundle-regardless-of-current-default-locale)
// ResourceBundle resourceBundle = ResourceBundle.getBundle("ResourceBundleMessage", Locale.ROOT);
// Obtain the appropriate ResourceBundle for the Locale
ResourceBundle resourceBundle = ResourceBundle.getBundle("message", locale, I18nUtil.getResourceBundleClassLoader());
// Message key
return resourceBundle.getString("hello");
Only three lines are needed. Create message_languageCode_countryCode.properties files for different locales under the project’s resource directory.
- message_zh_CN.properties
- message_en_US.properties
Hibernate Validator uses the same approach, but the i18n file prefix must be ValidationMessages. Why? Because the Bean Validation specification requires it. If you do not want to use the name ValidationMessages, implement your own MessageInterpolator. See the Hibernate Validator documentation and the Bean Validation specification for implementation details. I will not explore this further here.
Another, less common approach implements internationalization by extending ResourceBundle. Here is an example from org.postgresql.translation in the PostgreSQL Maven dependency.
public class messages_zh_CN extends java.util.ResourceBundle {}
Pass package.baseClassName when using it.
// For the subclass-based approach, see java.util.ResourceBundle.ResourceBundleProviderHelper#loadResourceBundle
bundle = ResourceBundle.getBundle("org.postgresql.translation.messages", Locale.getDefault(Locale.Category.DISPLAY));
Define Message Keys
Enclose the key in {}.
@Getter
@Setter
public class Person {
@Size(min = 3, max = 5, message = "{userName.invalid}")
private String userName;
@Max(value = 10, message = "{userAge.invalid}")
private Integer userAge;
}
Define ValidationMessages Files
For testing, define ValidationMessages_zh_CN.properties and ValidationMessages_en_US.properties, both under the resource directory.
- Contents of ValidationMessages_en_US.properties
userName.invalid= userName ${validatedValue} is invalid userAge.invalid= max value is {value}, but current value is ${validatedValue} - Contents of ValidationMessages_zh_CN.properties
Convert the Chinese characters to Unicode escapes.userName.invalid=\u7528\u6237\u540d ${validatedValue} \u662f\u975e\u6cd5\u7684 userAge.invalid=\u6700\u5927\u503c\u662f {value}, \u4f46\u662f\u5f53\u524d\u503c\u662f ${validatedValue}
The actual message text after each key contains two special values in these examples:
- ${validatedValue} represents the current value. Use this exact variable name to access it. You can also use EL expressions.
- {value} refers to a constraint annotation attribute. Depending on the annotation, it may be value, min, max, or another attribute.
Message parameters are string literals enclosed in {}, while message expressions are string literals enclosed in ${}
Hibernate Validator’s source contains many ValidationMessages files too. Will our custom files override them? Generally, those files contain the default messages for constraint annotations, so our custom keys do not conflict with them. For key lookup rules, see Hibernate Validator’s message interpolation documentation and Bean Validation’s message interpolation specification.
Verify Internationalized Error Messages
This article uses RESTEasy rather than Spring MVC as its RESTful web service framework. Start the application and call it with curl.
curl --header "Accept-Language: en-US" --json '{"userName":"tty", "userAge":40}' http://localhost:8080/validation-demo/valid/inline
curl --header "Accept-Language: zh-CN" --json '{"userName":"tty", "userAge":40}' http://localhost:8080/validation-demo/valid/inline
The returned error messages are localized according to the Accept-Language request header.
How Does RESTEasy Do This?
Hibernate Validator explains how to use internationalized messages, but does not implement switching according to different Accept-Language headers. With that question in mind, I examined RESTEasy’s source. According to the validation section of the RESTEasy documentation, its Hibernate Validator integration is implemented in the resteasy-validator-provider JAR. Looking through the code:
-
Among implementations of MessageInterpolator, RESTEasy provides LocaleSpecificMessageInterpolator.
-
org.jboss.resteasy.plugins.validation.GeneralValidatorImpl#getValidator instantiates LocaleSpecificMessageInterpolator.
getLocale obtains the Locale from Accept-Language. That Locale and the current MessageInterpolator are passed in to change the locale used by interpolation. RESTEasy handles this neatly: since only the locale needs to change, LocaleSpecificMessageInterpolator wraps the existing MessageInterpolator and receives the locale through its constructor.The following two lines are important. ValidatorFactory creates Validator instances. A new MessageInterpolator has already been created for the locale, so ValidatorFactory must now use it to create a Validator instance. usingContext() serves this purpose. Bean Validation documentation
ValidatorContext returned by usingContext() can be used to customize the state in which the Validator must be initialized. This is used to customize the MessageInterpolator, the TraversableResolver, the ParameterNameProvider, the ClockProvider or the ConstraintValidatorFactory
Hibernate Validator documentation
When working with a configured validator factory it can occasionally be required to apply a different configuration to a single Validator instance. Example 9.28, “Configuring a Validator instance via usingContext()” shows how this can be achieved by calling ValidatorFactory#usingContext()
-
Once the Validator instance is obtained, the relevant method is called. Here is a screenshot:

-
Where is GeneralValidatorImpl instantiated? Look at the calls to its constructor.
The two screenshots above show that ValidatorContextResolver is a Provider implementing ContextResolver. A request enters this Provider, which creates the new MessageInterpolator, obtains the corresponding Validator instance, and performs validation. For more on ContextResolver, see the JAX-RS specification.
Hibernate Validator Initialization
This project uses Weld CDI. During initialization, Weld CDI loads the injection SPI, which triggers the SPI in hibernate-validator-cdi.

RESTEasy also creates a ValidatorFactory instance.
