0

Got a simple DTO for users of my app where I use jakarta validators for the length of the password.

public class UserDto {

  @NonNull
  @NotBlank private String username;

  @JsonProperty(access = WRITE_ONLY)
  @NonNull
  @ToString.Exclude
  @Size(min = 8, max = 30, message = "Please provide a password between 8 and 30 characters long.")
  private String password;
}

I'm making sure that I use the @Valid annotation on the controller for registering users:

@PostMapping(value = "/register")
public ResponseEntity<UserDto> registerUser(@RequestBody @Valid UserDto user) {
  return new ResponseEntity<>(userDetailsService.save(user), HttpStatus.CREATED);
}

Now, if I try to register a user with a password that is too short, e.g the password "pass":

{
  "username": "memeuser",
  "password": "pass"
}

I get a 400 and a pretty verbose error message that reveals several API internals:

{
  "message": "Validation failed for argument [0] in public org.springframework.http.ResponseEntity<com.agilebank.model.user.UserDto> com.agilebank.controller.JwtAuthenticationController.registerUser(com.agilebank.model.user.UserDto): [Field error in object 'userDto' on field 'password': rejected value [pass]; codes [Size.userDto.password,Size.password,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDto.password,password]; arguments []; default message [password],30,8]; default message [Please provide a password between 8 and 30 characters long.]] "
}

I am decorating the Exception that is thrown through the following class and method:

@RestControllerAdvice
public class ExceptionAdvice {

  @ResponseBody
  @ExceptionHandler({
    HttpMessageNotReadableException.class,
    InsufficientBalanceException.class,
    SameAccountException.class,
    InvalidTransactionCurrencyException.class,
    OneOfTwoCurrenciesMissingException.class,
    InvalidSortByFieldSpecifiedException.class,
    MethodArgumentNotValidException.class // <---- Here is the type that interests us
  })
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public ResponseEntity<ExceptionMessageContainer> badRequestStatusMessage(Exception exc) {
    return new ResponseEntity<>(new ExceptionMessageContainer(exc.getMessage()), HttpStatus.BAD_REQUEST);
  }
}

where ExceptionMessageContainer is a simple container for the exception message:

@AllArgsConstructor
public class ExceptionMessageContainer {
    private String message;
}

Looking at the official docs for this exception type I am not so sure there's a straightforward way to get just the message that I have inside the @Size constraint, other than processing the String that getMessage() returns for me, which is not a generalizable solution. Any ideas about how I might be able to get just the message that I provide at the level of the constraint? I could of course also just target instances of MethodArgumentNotValidException through a separate method inside ExceptionAdvice and have something like:

 @ResponseBody
 @ExceptionHandler({
    MethodArgumentNotValidException.class
  })
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public ResponseEntity<ExceptionMessageContainer> methodArgumentNotValidExceptionSpecificHandler(MethodArgumentNotValidException exc) {
    return new ResponseEntity<>(new ExceptionMessageContainer("Password should be between 8 and 30 characters long"), HttpStatus.BAD_REQUEST);
  }

but there are other cases where this exception type might be thrown, since I have other DTOs with different validator constraints which can throw this exception type. So this would not work well in general and would mislead the user.

Jason
  • 2,495
  • 4
  • 26
  • 37
  • Why did you decide to write your own exception handler? The Spring one will just print out the messages. Next to that if you want something special for this case (the `MethodArgumentNotValidException.class`) then write it. The exception contains the actual messages. – M. Deinum Jul 19 '23 at 18:23
  • The reason behind writing my own exception handler has more to do with ensuring that a 400 `BAD_REQUEST` Http code is sent, rather than with the message itself. You are correct about the exception containing the actual messages. Is there any way to get a more user-friendly message _OTHER_ than parsing the string with `substr()`, `lastIndexOf()`, etc? – Jason Jul 22 '23 at 18:55
  • You don't need to. The fields are there, you need to change your exception handling for the `MethodArgumentNotValidException`. That includes ALL messages and even transposed already. – M. Deinum Jul 24 '23 at 06:20

0 Answers0