How to return the ConstraintViolationException messages in the response body?
Asked Answered
P

2

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.

Pronouncement answered 15/10, 2019 at 17:36 Comment(4)
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)?Pronouncement
Have you checked if birthDate is null?Lambdacism
Yes (see above). There's no exception caught in my logs or while debugging.Pronouncement
On another thread, someone reminded us that the RestController class also needs the "@Validated" annotation.Towbin
P
9

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.

Pronouncement answered 16/10, 2019 at 15:52 Comment(2)
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 !Aschim
With Spring Boot 3 this doesn't seem to work anymore.Zeralda
D
1

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.

Deedeeann answered 20/1, 2021 at 10:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.