Search code examples
spring-bootvalidationdate-format

Springboot show error message for invalid date (YearMonth) formats: eg 2020-15


I have a project with Spring Boot and I want to show an error response if the given date format is incorrect. The correct format is yyyy-MM (java.time.YearMonth) but I want to want to show a message if someone sends 2020-13, 2020-111 or 2020-1.

When I've added a custom validator the debugger goes in there with a valid request but not with an incorrect request. I also tried to use the message.properties with the typeMismatch.project.startdate=Please enter a valid date. but I also don't see that message in my response body. It seems like the application does not understand my incorrect request and then always throws a BAD REQUEST with empty body, which is not strange because it is not a valid date.

Can someone explain me how I can show an errormessage in the response for these incorrect values? Or is there no other way then use a String and convert that to the YearMonth object so I can show catch and show an error message?

Request object:

@Getter
@Setter    
public class Project {
    @NotNull(message = "mandatory")
    @DateTimeFormat(pattern = "yyyy-MM")
    private YearMonth startdate;
}

Controller:

@RestController
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public class ProjectController {

    @PostMapping(value = "/project", consumes = MediaType.APPLICATION_JSON_VALUE)
    public Project newProject(@Valid @RequestBody Project newProject) {
        return projectService.newProject(newProject);
    }
}

ExceptionHandler:

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @SneakyThrows
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        headers.add("Content-Type", "application/json");

        ObjectMapper mapper = new ObjectMapper();

        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String name;
            if (error instanceof FieldError)
                name = ((FieldError) error).getField();
            else
                name = error.getObjectName();
            String errorMessage = error.getDefaultMessage();
            errors.put(name, errorMessage);
        });

        return new ResponseEntity<>(mapper.writeValueAsString(errors), headers, status);
    }
}

Solution

  • Okay, I made a solution which is workable for me. I've added the solution below for people who find this thread in the future and has the same problem I had.

    Create a custom validator with a simple regex pattern:

    @Target({ FIELD })
    @Retention(RUNTIME)
    @Constraint(validatedBy = YearMonthValidator.class)
    @Documented
    public @interface YearMonthPattern {
    
        String message() default "{YearMonth.invalid}";
    
        Class<?>[] groups() default { };
    
        Class<? extends Payload>[] payload() default { };
    
    }
    
    public class YearMonthValidator implements ConstraintValidator<YearMonthPattern, String> {
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            Pattern pattern = Pattern.compile("^([0-9]{4})-([0-9]{2})$");
            Matcher matcher = pattern.matcher(value);
            try {
                return matcher.matches();
            } catch (Exception e) {
                return false;
            }
        }
    }
    

    Update the request object:

    @Getter
    @Setter
    public class Project {
        @NotNull(message = "mandatory")
        @YearMonthPattern
        private String startdate;
        
        public YearMonth toYearMonth(){
            return YearMonth.parse(startdate);
        }
    }
    

    The DateTimeFormat annotation is replaced with our new custom validator and instead of a YearMonth, make it a String. Now the validator annotation can be executed because the mapping to the YearMonth won't fail anymore.

    We also add a new method to convert the String startdate to a YearMonth after Spring has validated the request body, so we can use it in the service as a YearMonth instead of having to translate it each time.

    Now when we send a requestbody with:

    {
        "startdate": "2020-1"
    }
    

    we get a nice 400 bad request with the following response:

    {
        "endDate": "{YearMonth.invalid}"
    }