6

I am developing a Spring Boot based REST API. I am validating the input entities using custom ConstraintValidator annotations. My problem is that I cannot return the ConstraintViolationException messages in the response. My exception handler does not catch the exceptions (maybe because they're wrapped in another types of exceptions).

Can I please get some advice on how to handle the situation?

I've searched all over the Internet but I couldn't find a fitting solution for me and I've also wasted some hours doing so.

Example annotation:

@Documented
@Retention(RUNTIME)
@Target({FIELD, PARAMETER})
@Constraint(validatedBy = BirthDateValidator.class)
public @interface ValidBirthDate {

    String message() default "The birth date is not valid.";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

Validator class:

public class BirthDateValidator extends FieldValidationBase implements ConstraintValidator<ValidBirthDate, LocalDate> {

    private static final Logger LOGGER = LoggerFactory.getLogger(BirthDateValidator.class);

    @Override
    public void initialize(ValidBirthDate constraintAnnotation) {
    }

    @Override
    public boolean isValid(LocalDate birthDate, ConstraintValidatorContext constraintValidatorContext) {
        constraintValidatorContext.disableDefaultConstraintViolation();
        LOGGER.info("Starting the validation process for birth date {}.", birthDate);

        if(birthDate == null) {
            constraintValidatorContext.buildConstraintViolationWithTemplate("The birth date is null.")
                    .addConstraintViolation();
            return false;
        }

        //other validations

        return true;
    }

Model class:

public class Manager extends BaseUser {

    //other fields 

    @Valid
    @ValidBirthDate
    private LocalDate birthDate;

    //setters & getters

Exception handler:

@ExceptionHandler(value = ConstraintViolationException.class)
    public ResponseEntity handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
        List<String> errors = new ArrayList<>();

        for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
            errors.add(violation.getRootBeanClass().getName() + ": " + violation.getMessage());
        }

        Error response = new Error(errors);
        return new ResponseEntity<Object>(response, new HttpHeaders(), BAD_REQUEST);
    }

The controller:

@RestController
@RequestMapping(value = "/register", consumes = "application/json", produces = "application/json")
public class RegistrationController {

    @Autowired
    private RegistrationService registrationService;

    @PostMapping(value = "/manager")
    public ResponseEntity registerManager(@RequestBody @Valid Manager manager) {
        registrationService.executeSelfUserRegistration(manager);
        return new ResponseEntity<>(new Message("User " + manager.getEmailAddress() + " registered successfully!"), CREATED);
    }
}

I get the 400 response code, but I am not seeing any response body containing the violated constraint messages.

Adrian Pop
  • 215
  • 1
  • 3
  • 12
  • I also have a generic exception handler (for `Exception`), but that doesn't catch it either. I assume the exception is handles internally by Spring and wrapped into something else, but I have no idea what that is. Shouldn't the `ConstraintViolationException` be thrown when the object fails the validation (`isValid()` returns false)? – Adrian Pop Oct 15 '19 at 17:43
  • Have you checked if `birthDate` is null? – Jens Oct 15 '19 at 17:46
  • Yes (see above). There's no exception caught in my logs or while debugging. – Adrian Pop Oct 15 '19 at 17:49
  • On another thread, someone reminded us that the RestController class also needs the "@Validated" annotation. – allenjom Nov 20 '19 at 16:54

2 Answers2

8

After some more debugging, I found out that all constraint violations were wrapped into a MethodArgumentNotValidException (because of the @Valid annotations) - I had to dig a bit inside that exception to get my information.

I've overriden the handleMethodArgumentNotValid() method from ResponseEntityExceptionHandler and this is how I got it to work:

@Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {

        List<String> errorMessages = new ArrayList<>();
        BindingResult bindingResult = ex.getBindingResult();
        List<ObjectError> errors = bindingResult.getAllErrors();
        for(ObjectError error : errors) {
            String message = error.getDefaultMessage();
            errorMessages.add(message);
        }

        return new ResponseEntity<>(new Error(errorMessages), new HttpHeaders(), BAD_REQUEST);
    }

Maybe this helps someone.

Adrian Pop
  • 215
  • 1
  • 3
  • 12
  • This was really useful, I was struggling to extract the exact constraint violation and MethodArgumentNotValidException is not providing me the required result since it generic. I am able to dig in to the constraint error after I provided the code like this. Thank you very much ! – Sreedhar S Dec 10 '20 at 22:24
  • With Spring Boot 3 this doesn't seem to work anymore. – dbaltor Apr 05 '23 at 11:53
0

When the target argument fails to pass the validation, Spring Boot throws a MethodArgumentNotValidException exception. I have extracted the error message from bindingResult of this exception as shown below:

@RestControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

@Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex, HttpHeaders headers,
            HttpStatus status, WebRequest request) {
        //to extract the default error message from a diagnostic
        // information about the errors held in MethodArgumentNotValidException
        Exception exception = new Exception(ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        return this.createResponseEntity(HttpStatus.BAD_REQUEST, exception, request);
    }

private ResponseEntity<Object> createResponseEntity(
            HttpStatus httpStatus, Exception ex, WebRequest request) {
        ErrorResponse errorResponse = ErrorResponse.builder()
                .timestamp(LocalDateTime.now())
                .status(httpStatus.value())
                .error(httpStatus.getReasonPhrase())
                .message(ex.getMessage())
                .path(request.getDescription(true))
                .build();
        return handleExceptionInternal(ex, errorResponse,
                new HttpHeaders(), httpStatus, request);
    }

}

ErrorResponse class:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResponse {

    private LocalDateTime timestamp;
    private int status;
    private String error;
    private String message;
    private String path;
}

The response will be 400 with body in JSON format as shown below:

{
  "timestamp": "2021-01-20T10:30:15.011468",
  "status": 400,
  "error": "Bad Request",
  "message": "Due date should not be greater than or equal to Repeat Until Date.",
  "path": "uri=/api/tasks;client=172.18.0.5;user=109634489423695603526"
}

I hope this helps. If you need a detailed explanation on class-level constraint, have a look at this video.