I use jakarta's @AssertTrue
on my object when I save it on the page to check if the phone number matches the mask:
@AssertTrue(message = "{validation.phone.incorrect}")
public boolean isCorrectPhone() {
String phoneRegex = "^(\\+\\d{1,3}( )?)?((\\(\\d{1,3}\\))|\\d{1,3})[- .]?\\d{3,4}[- .]?\\d{4}$";
return phone == null || phone.matches(phoneRegex);
}
The problem is that when the phone has incorrect format I get not the message that I expect, but a very detailed one with packages and classes used. And the message that I need is available only at the end of this message.
My expected message is: "Incorrect phone number".
What I get in exception looks like this:
Validation failed for argument [0] in public void com.my.user.UserController.editUser(com.my.entity.user.User): [Field error in object 'user' on field 'correctPhone': rejected value [false]; codes [AssertTrue.user.correctPhone,AssertTrue.correctPhone,AssertTrue]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.correctPhone,correctPhone]; arguments []; default message [correctPhone]]; default message [Incorrect phone number]]
.getCause()
on the exception returns null
, so there's no any nested exceptions with cleaner message. How do I get the message I want without parsing?
The problem was partly with our custom exception handlers one of which intercepted MethodArgumentNotValidException
and wrapped it into Error
(our custom, not java.lang) like this:
return new Error(ErrorCode.NOT_VALID, ex.getMessage());
So, this was the message I got. But as I said before I examined the exception there and saw that it doesn't have any root causes with clean message. I actually even tried to remove this handler at all to get a response like first snippet here but got even worse result:
{
"timestamp": "2025-03-07T09:05:40.792+00:00",
"status": 400,
"error": "Bad Request",
"path": "/api/users/edit"
}
No message at all! Just some generic error. I double checked: no other exception handlers intercepted this exception, so it was "clean" output.
But then I found out that actually MethodArgumentNotValidException
has a method getFieldErrors()
(I don't know why I didn't check for all available methods beforehand). This method contains a list of errors for all fields that are marked with either validation annotation. So, all I had to do is to loop through all field errors and get what I need, smth like:
return ex.getFieldErrors().stream()
.map(error -> new FieldValidationError(error.getField(), error.getDefaultMessage(), error.getRejectedValue()))
.toList();