I'm developing an API with Spring Boot and currently, I'm thinking about how to handle error messages in an easily internationalizable way. My goals are as follows:
- Define error messages in resource files/bundles
- Connect constraint annotation with error messages (e.g.,
@Length
) in a declarative fashion - Error messages contain placeholders, such as
{min}
, that are replaced by the corresponding value from the annotation, if available, e.g.,@Length(min = 5, message = msg)
would result in something likemsg.replace("{min}", annotation.min()).replace("{max}", annotation.max())
. - The JSON property path is also available as a placeholder and automatically inserted into the error message when a validation error occurs.
- A solution outside of an error handler is preferred, i.e., when the exceptions arrive in the error handler, they already contain the desired error messages.
- Error messages from a resource bundle are automatically registered as constants in Java.
Currently, I customized the methodArgumentNotValidHandler
of my error handler class to read ObjectError
s from e.getBindingResult().getAllErrors()
and then try to extract their arguments and error codes to decide which error message to choose from my resource bundle and format it accordingly. A rough sketch of my code looks as follows:
Input:
@Data
@RequiredArgsConstructor
public class RequestBody {
@NotNull
@NotBlank(message = ErrorConstants.NOT_BLANK)
@Length(min = 5, max = 255, message = ErrorConstants.LENGTH_MIN_MAX) // LENGTH_MIN_MAX = validation.length.min-max
private String greeting;
}
Error handler:
@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
ErrorMessage methodArgumentNotValidHandler(MethodArgumentNotValidException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
Object[] arguments = objectError.getArguments();
String messageCode = objectError.getDefaultMessage(); // e.g., "validation.length.min-max" (key in resource bundle)
ResourceBundle errMsgBundle = ResourceBundle.getBundle("errorMsg");
String message;
if (objectError.getCode().equals("Length")) {
String messageTemplate = errMsgBundle.getString(messageCode);
message = String.format(messageTemplate, arguments[2], arguments[1]);
} else {
message = "Bad input, but I cannot tell you the problem because the programmer hasn't handled this yet. Sorry :'(";
}
return new ErrorMessage(message);
}
Unfortunately, I suppose this approach is not maintainable. In the error handler, I will end up with a huge if-else block that has to probe several different situations (error codes, number of arguments, ...) and format error messages accordingly. Changing error messages will possibly result in having to change the code (e.g., the order of arguments). Each property key must be present as a constant in ErrorConstants
, which I find undesirable. This code also doesn't query the name or path of the faulty property, e.g., "name".
Hence,
- is there a solution that can satisfy some or all of the above-mentioned requirements?
- At which place would I implement this?
- Is there at least a better solution to the above one?
- Are there recipes or patterns in SpringBoot to handle validation errors (I'm definitely not the first one thinking about this)?