javaspring-bootvalidation

Spring boot validation problem: understanding the MethodArgumentNotValidException


I have a problem when validating a form with Spring Boot. I have already found the cause of the problem (and know a possible solution), but I would like to understand why it happens.

In the controller that the post method reaches I have:

@RequestMapping(value = "/add", method = RequestMethod.POST)
public String processAddNewEvent(@Valid @ModelAttribute("newEvent") EventDto newEvent, @ModelAttribute("imageFile") MultipartFile imageFile, BindingResult result, HttpServletRequest request) {

    if(result.hasErrors()) {
        return "addEvent";
    }
    
    return "redirect:/events";
}

What I want is just to validate the newEvent object. If it has errors, I show the "addEvent" form again with error messages.

But in this method, I receive two parameters from the form: the newEvent object and a file (imageFile). When this is the case, as soon as this function is called I get a MethodArgumentNotValidException. However, if I remove the imageFile and only receive the newEvent object, then the exception doesn't get thrown and the validation works correctly.

That is, if I the controller looks like this, the validation works and I don´t have exception:

@RequestMapping(value = "/add", method = RequestMethod.POST)
public String processAddNewEvent(@Valid @ModelAttribute("newEvent") EventDto newEvent, BindingResult result, HttpServletRequest request) {
    if(result.hasErrors()) {
        return "addEvent";
    }
    
    return "redirect:/events";
}

So, my questions are, Can validation be done on one object and not the other? Shouldn't I receive two different objects from the form? What would be the correct way to do so?

Thanks and sorry for my English!

My idea to solve it is to put the image file inside the newEvent class, and thus receive only one object. I still don't know if it will work. I just want to understand the problem well


Solution

  • your issue arises because Spring tries to validate both objects (newEvent and imageFile) due to the presence of @Valid on newEvent, but only newEvent is eligible for validation. MultipartFile isn't meant to be validated with the standard bean validation mechanism (like @Valid or @NotNull), and thus causes the MethodArgumentNotValidException.

    Can validation be done on one object and not the other? Yes, validation can be applied only to the newEvent object. You are correctly using @Valid on newEvent, which triggers the validation of that specific object. MultipartFile, however, should be handled separately, without applying validation annotations.

    Shouldn't I receive two different objects from the form? Yes, you can receive two separate objects: newEvent (the DTO) and MultipartFile (the file). It's perfectly fine to handle them separately as long as you're not trying to validate the MultipartFile object in the same way as newEvent.

    Solution change the order of the parameters of method processAddNewEvent, like this:

        @RequestMapping(value = "/add", method = RequestMethod.POST)
        public String processAddNewEvent(
        @Valid @ModelAttribute("newEvent") EventDto newEvent, 
        BindingResult result, 
        @ModelAttribute("imageFile") MultipartFile imageFile, 
        HttpServletRequest request) {
    
        if (result.hasErrors()) {
            return "addEvent"; // Return the form view with error messages
        }
    
        // Handle the imageFile separately (e.g., save the file, validate its size/type)
        // Validate the imageFile
        if (!imageFile.isEmpty()) {
            // Example: Check the file size (e.g., max 2MB)
            long maxFileSize = 2 * 1024 * 1024; // 2 MB
            if (imageFile.getSize() > maxFileSize) {
                result.rejectValue("imageFile", "error.imageFile", "File size exceeds the 2MB limit.");
                return "addEvent";
            }
    
            // Example: Check the file type (e.g., only accept JPEG and PNG)
            String contentType = imageFile.getContentType();
            if (!"image/jpeg".equals(contentType) && !"image/png".equals(contentType)) {
                result.rejectValue("imageFile", "error.imageFile", "Only JPEG and PNG files are accepted.");
                return "addEvent";
            }
    
            // Save the image file to a specific directory
            try {
                String uploadDirectory = "/path/to/upload/directory"; // Replace with actual path
                String fileName = imageFile.getOriginalFilename();
                Path path = Paths.get(uploadDirectory + File.separator + fileName);
                Files.copy(imageFile.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
            } catch (IOException e) {
                result.rejectValue("imageFile", "error.imageFile", "Failed to save the file. Please try again.");
                return "addEvent";
            }
        }
        
        return "redirect:/events";
    }