|
For the latest stable version, please use Spring Framework 6.2.12! |
Validation by Using Spring’s Validator Interface
Spring features a Validator interface that you can use to validate objects. The
Validator interface works by using an Errors object so that, while validating,
validators can report validation failures to the Errors object.
Consider the following example of a small data object:
-
Java
-
Kotlin
public class Person {
private String name;
private int age;
// the usual getters and setters...
}
class Person(val name: String, val age: Int)
The next example provides validation behavior for the Person class by implementing the
following two methods of the org.springframework.validation.Validator interface:
-
supports(Class): Can thisValidatorvalidate instances of the suppliedClass? -
validate(Object, org.springframework.validation.Errors): Validates the given object and, in case of validation errors, registers those with the givenErrorsobject.
Implementing a Validator is fairly straightforward, especially when you know of the
ValidationUtils helper class that the Spring Framework also provides. The following
example implements Validator for Person instances:
-
Java
-
Kotlin
public class PersonValidator implements Validator {
/**
* This Validator validates only Person instances
*/
public boolean supports(Class clazz) {
return Person.class.equals(clazz);
}
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
Person p = (Person) obj;
if (p.getAge() < 0) {
e.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) {
e.rejectValue("age", "too.darn.old");
}
}
}
class PersonValidator : Validator {
/**
* This Validator validates only Person instances
*/
override fun supports(clazz: Class<*>): Boolean {
return Person::class.java == clazz
}
override fun validate(obj: Any, e: Errors) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty")
val p = obj as Person
if (p.age < 0) {
e.rejectValue("age", "negativevalue")
} else if (p.age > 110) {
e.rejectValue("age", "too.darn.old")
}
}
}
The static rejectIfEmpty(..) method on the ValidationUtils class is used to
reject the name property if it is null or the empty string. Have a look at the
ValidationUtils javadoc
to see what functionality it provides besides the example shown previously.
While it is certainly possible to implement a single Validator class to validate each
of the nested objects in a rich object, it may be better to encapsulate the validation
logic for each nested class of object in its own Validator implementation. A simple
example of a “rich” object would be a Customer that is composed of two String
properties (a first and a second name) and a complex Address object. Address objects
may be used independently of Customer objects, so a distinct AddressValidator
has been implemented. If you want your CustomerValidator to reuse the logic contained
within the AddressValidator class without resorting to copy-and-paste, you can
dependency-inject or instantiate an AddressValidator within your CustomerValidator,
as the following example shows:
-
Java
-
Kotlin
public class CustomerValidator implements Validator {
private final Validator addressValidator;
public CustomerValidator(Validator addressValidator) {
if (addressValidator == null) {
throw new IllegalArgumentException("The supplied [Validator] is " +
"required and must not be null.");
}
if (!addressValidator.supports(Address.class)) {
throw new IllegalArgumentException("The supplied [Validator] must " +
"support the validation of [Address] instances.");
}
this.addressValidator = addressValidator;
}
/**
* This Validator validates Customer instances, and any subclasses of Customer too
*/
public boolean supports(Class clazz) {
return Customer.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
Customer customer = (Customer) target;
try {
errors.pushNestedPath("address");
ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
} finally {
errors.popNestedPath();
}
}
}
class CustomerValidator(private val addressValidator: Validator) : Validator {
init {
if (addressValidator == null) {
throw IllegalArgumentException("The supplied [Validator] is required and must not be null.")
}
if (!addressValidator.supports(Address::class.java)) {
throw IllegalArgumentException("The supplied [Validator] must support the validation of [Address] instances.")
}
}
/*
* This Validator validates Customer instances, and any subclasses of Customer too
*/
override fun supports(clazz: Class<>): Boolean {
return Customer::class.java.isAssignableFrom(clazz)
}
override fun validate(target: Any, errors: Errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required")
val customer = target as Customer
try {
errors.pushNestedPath("address")
ValidationUtils.invokeValidator(this.addressValidator, customer.address, errors)
} finally {
errors.popNestedPath()
}
}
}
Validation errors are reported to the Errors object passed to the validator. In the case
of Spring Web MVC, you can use the <spring:bind/> tag to inspect the error messages, but
you can also inspect the Errors object yourself. More information about the
methods it offers can be found in the javadoc.
Validators may also get locally invoked for the immediate validation of a given object,
not involving a binding process. As of 6.1, this has been simplified through a new
Validator.validateObject(Object) method which is available by default now, returning
a simple Errors representation which can be inspected: typically calling hasErrors()
or the new failOnError method for turning the error summary message into an exception
(e.g. validator.validateObject(myObject).failOnError(IllegalArgumentException::new)).