This short guide will focus on a single specific aspect of custom bean validation. If you need to catch up on how to write a custom bean validator, check out the tutorial on reflectoring.io. What is usually missing from these how-tos is the handling of validators for an entire class instead of just a field and how to set custom errors for specific field errors in a class.
Why would you want to write a validator for an entire class?
You may run into a situation where the value of one field of a class depends on the value of another field. For example, the field “type” value impacts which values are valid for the field “content”.
But when you define a custom validator, the validation annotation @interface only represents a single error message. The result is that any field error would result in the same error message. In a web service, this is not very helpful for users of your API.
Let’s start at the beginning. First, you need the annotation that links to the custom validator.
@Documented
@Target({ TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = AlbumValidator.class)
public @interface Album {
String message() default "The album does not match our level of quality.";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
Then, you annotate the class to validate with your new annotation.
@Data
@Album
@NoArgsConstructor
public class MusicAlbum {
...
}
Lastly, in your validator, you can manipulate the context to set your custom error messages for a specific field.
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("{album.error.invalid-genre}")
.addPropertyNode("genre")
.addConstraintViolation();
The first thing to do is to call disableDefaultConstraintViolation(). The documentation is pretty straightforward.
Disables the default ConstraintViolation object generation (which is using the message template declared on the constraint).
Useful to set a different violation message or generate a ConstraintViolation based on a different property.
Now, you can build your constraint violation messages. You have two options for setting a message.
- Store your messages in messages.properties files, so you can easily translate them. To refer to such messages, enclose the message code in curly braces, as shown in the example above.
- Pass the error message string directly to buildConstraintViolationWithTemplate() without curly braces.
For clarity, you should specify the field name that failed the validation with addPropertyNode() and, lastly, call addConstraintViolation(). If you don’t do that, you will get an error message.
javax.validation.ValidationException: HV000033: At least one custom message must be created if the default error message gets disabled.
As I did in the demo application, you can also set more than one constraint violation. Simply call buildConstraintViolationWithTemplate(), addPropertyNode(), and addConstraintViolation() as often as you need.
I have also added a custom Spring exception handler using @ControllerAdvice, where I demonstrate how you can inspect the MethodArgumentNotValidException. If you have set custom field errors as shown earlier, you can access them using getFieldErrors().
e.getFieldErrors().stream()
.map(this::buildMessageText)
.toList();
private String buildMessageText(FieldError error) {
if (null != error.getDefaultMessage()) {
return "%s: %s".formatted(error.getField(), error.getDefaultMessage());
} else {
return "Invalid value for '%s'.".formatted(error.getField());
}
}
If no field errors are set, the @interface default message is used. You can gain access to that using getGlobalError().
var error = e.getGlobalError();
messages = List.of(error.getDefaultMessage());
I hope this was useful. Thank you for reading.