@RequestBody @Valid SomeDTO has field of enum type, custom error message

13,252

Solution 1

@ControllerAdvice
public static class GenericExceptionHandlers extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException e, HttpHeaders headers, HttpStatus status, WebRequest request) {
        return new ResponseEntity<>(new ErrorDTO().setError(e.getMessage()), HttpStatus.BAD_REQUEST);
    }
}

I created a fully functional Spring boot Application with a Test on Bitbucket

Solution 2

You do not need @Valid for enum validation, you can achieve the required response using below code:

Controller Code, StackDTO has an enum PaymentType in it:

@RequestMapping(value = "/reviews", method = RequestMethod.POST)
    @ResponseBody
    public ResponseEntity<String> add(@RequestBody StackDTO review) {

        return new ResponseEntity<String>(HttpStatus.ACCEPTED);
    }

Create an exception class, as EnumValidationException

public class EnumValidationException extends Exception {

    private String enumValue = null;
    private String enumName = null;

    public String getEnumValue() {
        return enumValue;
    }

    public void setEnumValue(String enumValue) {
        this.enumValue = enumValue;
    }

    public String getEnumName() {
        return enumName;
    }

    public void setEnumName(String enumName) {
        this.enumName = enumName;
    }

    public EnumValidationException(String enumValue, String enumName) {
        super(enumValue);

        this.enumValue = enumValue;
        this.enumName = enumName;
    }

    public EnumValidationException(String enumValue, String enumName, Throwable cause) {
        super(enumValue, cause);

        this.enumValue = enumValue;
        this.enumName = enumName;
    }
}

I have enum as below, with a special annotation @JsonCreator on a method create

public enum PaymentType {

    CREDIT("Credit"), DEBIT("Debit"); 

    private final String type;

    PaymentType(String type) {
        this.type = type;
    }

    String getType() {
        return type;
    }

    @Override
    public String toString() {
        return type;
    }

    @JsonCreator
    public static PaymentType create (String value) throws EnumValidationException {
        if(value == null) {
            throw new EnumValidationException(value, "PaymentType");
        }
        for(PaymentType v : values()) {
            if(value.equals(v.getType())) {
                return v;
            }
        }
        throw new EnumValidationException(value, "PaymentType");
    }
}

Finally RestErrorHandler class,

@ControllerAdvice
public class RestErrorHandler {

    @ExceptionHandler(HttpMessageNotReadableException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public ResponseEntity<ValidationErrorDTO> processValidationIllegalError(HttpMessageNotReadableException ex,
            HandlerMethod handlerMethod, WebRequest webRequest) {

        EnumValidationException exception = (EnumValidationException) ex.getMostSpecificCause();

        ValidationErrorDTO errorDTO = new ValidationErrorDTO();
        errorDTO.setEnumName(exception.getEnumName());
        errorDTO.setEnumValue(exception.getEnumValue());
        errorDTO.setErrorMessage(exception.getEnumValue() + " is an invalid " + exception.getEnumName());
        return new ResponseEntity<ValidationErrorDTO>(errorDTO, HttpStatus.BAD_REQUEST);
    }

}

ValidationErrorDTO is the dto with setters/getters of enumValue, enumName and errorMessage. Now when you send POST call to controller endpoint /reviews with below request

{"paymentType":"Credit2"}

Then code returns response as 400 with below response body -

{
    "enumValue": "Credit2",
    "enumName": "PaymentType",
    "errorMessage": "Credit2 is an invalid PaymentType"
}

Let me know if it resolves your issue.

Solution 3

The answer provided by @Amit is good and works. You can go ahead with that if you want to deserialize an enum in a specific way. But that solution is not scalable. Every enum which needs validation must be annotated with @JsonCreator.

Other answers won't help you beautify the error message.

So here's my solution generic to all the enums in spring web environment.

@RestControllerAdvice
public class ControllerErrorHandler extends ResponseEntityExceptionHandler {
    public static final String BAD_REQUEST = "BAD_REQUEST";
    @Override
    public ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException exception,
                                                               HttpHeaders headers, HttpStatus status, WebRequest request) {
        String genericMessage = "Unacceptable JSON " + exception.getMessage();
        String errorDetails = genericMessage;

        if (exception.getCause() instanceof InvalidFormatException) {
            InvalidFormatException ifx = (InvalidFormatException) exception.getCause();
            if (ifx.getTargetType()!=null && ifx.getTargetType().isEnum()) {
                errorDetails = String.format("Invalid enum value: '%s' for the field: '%s'. The value must be one of: %s.",
                        ifx.getValue(), ifx.getPath().get(ifx.getPath().size()-1).getFieldName(), Arrays.toString(ifx.getTargetType().getEnumConstants()));
            }
        }
        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setTitle(BAD_REQUEST);
        errorResponse.setDetail(errorDetails);
        return handleExceptionInternal(exception, errorResponse, headers, HttpStatus.BAD_REQUEST, request);
    }

}

This will handle all the invalid enum values of all types and provides a better error message for the end user.

Sample output:

{
    "title": "BAD_REQUEST",
    "detail": "Invalid enum value: 'INTERNET_BANKING' for the field: 'paymentType'. The value must be one of: [DEBIT, CREDIT]."
}

Solution 4

Yon can achieve this using @ControllerAdvice as follows

@org.springframework.web.bind.annotation.ExceptionHandler(value = {InvalidFormatException.class})
    public ResponseEntity handleIllegalArgumentException(InvalidFormatException exception) {

        return ResponseEntity.badRequest().body(exception.getMessage());
    }

Basically , the idea is to catch com.fasterxml.jackson.databind.exc.InvalidFormatException and handle it as per your requirement.

Share:
13,252

Related videos on Youtube

timpham
Author by

timpham

Updated on October 13, 2022

Comments

  • timpham
    timpham over 1 year

    I have the following @RestController

    @RequestMapping(...)
    public ResponseEntity(@RequestBody @Valid SomeDTO, BindingResult errors) {
    //do something with errors if validation error occur
    }
    
    public class SomeDTO {
       public SomeEnum someEnum;
    }
    

    If the JSON request is { "someEnum": "valid value" }, everything works fine. However, if the request is { "someEnum": "invalid value" }, it only return error code 400.

    How can I trap this error so I can provide a custom error message, such as "someEnum must be of value A/B/C".

    • timpham
      timpham over 6 years
      @AmitKBist this doesn't answer the question on enum type
  • Arun Gowda
    Arun Gowda about 3 years
    I like the idea. But IMHO, it's not scalable. If there are 100 enums, I don't want to write @JsonCreator for all those enums.
  • Arun Gowda
    Arun Gowda about 3 years
    Can we make it more specific to Enums? HttpMessageNotReadableException is a very broad exception and will cover a lot of other exceptions like invalid json