28

If I use a constraint like this @NotNull and then in the controller

public User createUser(
            @Validated
            @RequestBody User user) {}

It gives a really nice 400 exception with details.

But if I use my own custom validator like this:

public User createUser(
            @UserConstraint
            @RequestBody User user) {}

It throws a 500 server error like this:

javax.validation.ConstraintViolationException: createUser.user: Error with field: 'test35'
    at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:117) ~[spring-context-5.1.10.RELEASE.jar:5.1.10.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.1.10.RELEASE.jar:5.1.10.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:69) ~[spring-security-core-5.1.6.RELEASE.jar:5.1.6.RELEASE]

Is there a way to get nice 400 message to the response?

Ideally the 400 message should be the same as Spring's validation JSON

{
    "timestamp": "2019-10-30T02:33:15.489+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "Size.user.lastName",
                "Size.lastName",
                "Size.java.lang.String",
                "Size"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.lastName",
                        "lastName"
                    ],
                    "arguments": null,
                    "defaultMessage": "lastName",
                    "code": "lastName"
                },
                25,
                1
            ],
            "defaultMessage": "size must be between 1 and 25",
            "objectName": "user",
            "field": "lastName",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "Size"
        }
    ],
    "message": "Validation failed for object='user'. Error count: 1",
    "path": "/api/v1/users"
}
erotsppa
  • 14,248
  • 33
  • 123
  • 181

3 Answers3

17

Yes, you can create a custom error handler so you can add anything on your response and status as well. This is the simple way to change the status:

1.- Simple way to change status when ConstraintViolationException is thrown.

import javax.validation.ConstraintViolationException;

@ControllerAdvice
public class CustomErrorHandler {

    @ExceptionHandler(ConstraintViolationException.class)
    public void handleConstraintViolationException(ConstraintViolationException exception,
            ServletWebRequest webRequest) throws IOException {
        webRequest.getResponse().sendError(HttpStatus.BAD_REQUEST.value(), exception.getMessage());
    }
}    

2.- Custom way to put the response when a ConstraintViolationException occurs.

@ControllerAdvice
public class CustomErrorHandler {

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<CustomError> handleConstraintViolationException(ConstraintViolationException exception) {
        CustomError customError = new CustomError();
        customError.setStatus(HttpStatus.BAD_REQUEST);
        customError.setMessage(exception.getMessage());
        customError.addConstraintErrors(exception.getConstraintViolations());
        return ResponseEntity.badRequest().body(customError);
    }
}   
Jonathan JOhx
  • 5,784
  • 2
  • 17
  • 33
  • 1
    I want to format the JSON as the same as spring's JSON. Do you know how to do that? I've edited the question to show the format – erotsppa Oct 30 '19 at 03:44
  • Hi @erotsppa, sorry for the delay, do you still want to do that? – Jonathan JOhx Jun 23 '20 at 23:52
  • @TheRealChx101 create a question so I can help you what you want to do – Jonathan JOhx Nov 06 '20 at 12:38
  • Thanks. I found the solution. You have to arrange your parameters in a certain order: `(@ModelAttribute Object, Errors, Model)` so that spring doesn't throw the exception – TheRealChx101 Nov 06 '20 at 16:09
  • 1
    this is not a good solution, this will make all `ConstraintViolationException ` return 400 when the question just wants the controller validation fail to return 400... it is important separate controller exceptions from datasource exceptions this solution brings a whole mess into exception handling for the whole project – Rafael Lima Sep 07 '22 at 16:36
13

As the solution above doesn't really produce the desired result here a link which might help: https://sterl.org/2020/02/spring-boot-hateoas-jsr303-validation/

Funny enough spring behaves differently if the class or the method request body is annotated with @Validated.

In other words on the class, you might encounter 500 errors. If you move the validation annotation, as you already did, into the method, the normal behavior should be 400.

Long story short, as soon as you have your custom contains, etc. you need to adjust the stuff a bit -- as in Spring, it is the MethodArgumentNotValidException and not the ConstraintViolationException, for which Spring already as a controller advice.

A quick solution may look like:

@Autowired
private MessageSource messageSource;

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
public @ResponseBody Map<String, Object> handleConstraintViolation(ConstraintViolationException e, ServletWebRequest request) {
    // emulate Spring DefaultErrorAttributes
    final Map<String, Object> result = new LinkedHashMap<>();
    result.put("timestamp", new Date());
    result.put("path", request.getRequest().getRequestURI());
    result.put("status", HttpStatus.BAD_REQUEST.value());
    result.put("error", HttpStatus.BAD_REQUEST.getReasonPhrase());
    result.put("message", e.getMessage());
    result.put("errors", e.getConstraintViolations().stream().map(cv -> SimpleObjectError.from(cv, messageSource, request.getLocale())));
    return result;
}

@Getter @ToString
static class SimpleObjectError {
    String defaultMessage;
    String objectName;
    String field;
    Object rejectedValue;
    String code;

    public static SimpleObjectError from(ConstraintViolation<?> violation, MessageSource msgSrc, Locale locale) {
        SimpleObjectError result = new SimpleObjectError();
        result.defaultMessage = msgSrc.getMessage(violation.getMessageTemplate(),
                new Object[] { violation.getLeafBean().getClass().getSimpleName(), violation.getPropertyPath().toString(),
                        violation.getInvalidValue() }, violation.getMessage(), locale);
        result.objectName = Introspector.decapitalize(violation.getRootBean().getClass().getSimpleName());
        result.field = String.valueOf(violation.getPropertyPath());
        result.rejectedValue = violation.getInvalidValue();
        result.code = violation.getMessageTemplate();
        return result;
    }
}
Paul
  • 180
  • 1
  • 8
2

Simply, define a method annotated with @ExceptionHandler in a class annotated with @ControllerAdvice:

@ControllerAdvice
public class YourControllerAdvice {

    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(ConstraintViolationException.class)
    public void handleConstraintViolationException() {
    // Intentionally left blank
    }
}

Classes annotated with @ControllerAdvice are used to deal with exceptions at controller level.

Hasan Can Saral
  • 2,950
  • 5
  • 43
  • 78
  • 2
    If you left method blank - response body will be blank. Also, you can extend this advice for variety of exceptions: `@ExceptionHandler(value = {ConstraintViolationException.class, ValidationException.class})` – chill appreciator Jul 24 '20 at 23:00