Skip to content
JackSparrow414
Go back

Using Hibernate Validator (Part 1)

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. Constraint validation and complete example code link in Hibernate Validator documentation The example code is clearly organized, with a directory for each chapter. Hibernate Validator example source directories organized by chapter

All Constraint Annotations

The Jakarta Bean Validation 3.0 specification lists all built-in annotations and their constraints.

Notes on @NotEmpty and @NotBlank

Notes on @Size

Where Constraint Annotations Can Be Placed

JavaBean

  1. field-level

    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...
    }
  2. 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;
    	}
    }
  3. Container-element constraints support List, Set, Map, and Optional.

  4. 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.

  5. 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

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

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.

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.

  1. Contents of ValidationMessages_en_US.properties
    	userName.invalid= userName ${validatedValue} is invalid
        userAge.invalid= max value is {value}, but current value is ${validatedValue}
  2. Contents of ValidationMessages_zh_CN.properties
    	userName.invalid=\u7528\u6237\u540d ${validatedValue} \u662f\u975e\u6cd5\u7684
        userAge.invalid=\u6700\u5927\u503c\u662f {value}, \u4f46\u662f\u5f53\u524d\u503c\u662f ${validatedValue}
    Convert the Chinese characters to Unicode escapes.

The actual message text after each key contains two special values in these examples:

  1. ${validatedValue} represents the current value. Use this exact variable name to access it. You can also use EL expressions.
  2. {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

Logs showing localized error messages returned after request validation fails 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:

  1. Among implementations of MessageInterpolator, RESTEasy provides LocaleSpecificMessageInterpolator.

  2. org.jboss.resteasy.plugins.validation.GeneralValidatorImpl#getValidator instantiates LocaleSpecificMessageInterpolator.

    LocaleSpecificMessageInterpolator wrapping an existing interpolator with a specified Locale RESTEasy getValidator configuring a message interpolator using the request Locale 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()

  3. Once the Validator instance is obtained, the relevant method is called. Here is a screenshot: RESTEasy source calling Validator to validate method parameters

  4. Where is GeneralValidatorImpl instantiated? Look at the calls to its constructor. Debugging ValidatorContextResolver returning a GeneralValidatorImpl instance ValidatorContextResolver annotated with @Provider and implementing ContextResolver 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. SPI registration and ValidationExtension debug information for hibernate-validator-cdi

ValidationExtension initializing the default ValidatorFactory and Validator in its constructor Debugging ValidatorContextResolver.getValidatorFactory obtaining the default validation factory RESTEasy also creates a ValidatorFactory instance. RESTEasy code creating a default ValidatorFactory when none exists

Notes


Share this post:

Previous Post
PostgreSQL (Part 2): Best Practices for Procedural Language Usage
Next Post
Shipping Tomcat Access Logs from EC2 to ELK with Filebeat and AWS CloudWatch Logs

Comments

Questions, corrections, and experiences are welcome. Sign in with GitHub to comment; both language versions share this discussion.

Comments are available on the live site only.