4

This is my controller. It accepts a multipart/form-data request with two fields, form and file. The form field is a MyObject, the file field is a MultipartFile. Both variables are annotated with @Valid, and accordingly, I would expect Spring to invoke the Validator class of each respective field. However, this only happens with MyObject, and not with MultipartFile.

@RequestMapping("/api")
@RestController
public class Controller {

    private MyObjectRepository repo;
    private MyObjectValidator myObjectValidator;
    private FileValidator fileValidator;

    @Autowired
    public myObjectController(MyObjectRepository repo, MyObjectValidator myObjectValidator,
                              FileValidator fileValidator) {
        this.repo = repo;
        this.myObjectValidator = myObjectValidator;
        this.fileValidator = fileValidator;
    }

    @InitBinder("form")
    public void initMyObjectBinder(WebDataBinder binder) {
        binder.setValidator(this.myObjectValidator);
    }

    @InitBinder("file")
    public void initFileBinder(WebDataBinder binder) {
        binder.setValidator(this.fileValidator);
    }

    @PostMapping("myObject")
    @ResponseStatus(HttpStatus.CREATED)
    @ResponseBody
    public MyObject createMyObject(@RequestPart("form") @Valid MyObject myObject,
                                   @RequestPart("file") @Valid MultipartFile... file) {
        return repo.save(myObject);
    }
}

My MyObjectValidator is triggered, but my FileValidator is not triggered. Both classes implement the Spring Validator interface. MyObjectValidator.supports(Class<?> aClass) is called, whereas FileValidator.supports(Class<?> aClass) is never called. Apart from that, my Controller is functioning perfectly, and diligently saves objects to my repo.

What could be the issue here? I've read similar questions, and common mistakes are to not use an appropriate argument inside the @InitBinder annotation, or to set the @InitBinder methods to private instead of public, but neither of this applies to my case.

This ugly workaround does what it's supposed to, but it is un-Spring-like. I call my FileValidator manually inside the Controller.createMyObject method, instead of letting Spring call it automatically through the @Valid annotation.

@PostMapping("myObject")
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
public MyObject createMyObject(@RequestPart("form") @Valid MyObject myObject,
                               @RequestPart("file") @Valid MultipartFile... file) {
    if (fileValidator.supports(file.getClass())) {
        Errors errors = new BeanPropertyBindingResult(file, "Uploaded file.");
        fileValidator.validate(file,errors);
        if (errors.hasErrors()) {
            throw new BadRequestException();
        }
    }
    return repo.save(myObject);
}

EDIT: I have included my Validator classes on request.

import org.springframework.validation.Validator;

public abstract class AbstractValidator implements Validator {
    // One shared method here.
}
public class FileValidator extends AbstractValidator {

    public boolean supports(Class<?> aClass) { // This method is never triggered.
        boolean isSingleFile = MultipartFile.class.isAssignableFrom(aClass); // This line has a breakpoint, it is never triggered in the debugger.
        boolean isFileArray = aClass.equals(MultipartFile[].class);
        return (isSingleFile || isFileArray);
    }

    public void validate(Object o, Errors e) {
        //Several validation methods go here.
    }
public class MyObjectValidator extends AbstractValidator {

    public boolean supports(Class<?> aClass) { // This method is triggered.
        return (MyObject.class.equals(aClass)); // This line has a breakpoint, and it is always triggered in the debugger.
    }

    public void validate(Object o, Errors e) {
        // Several validation methods go here.
    }

EDIT: I made some changes to my code like NiVeR suggested, removing the varargs parameter and changing my FileValidator.supports(Class<?> aClass) accordingly, but the behavior is still the same.

In Controller.java:

@PostMapping("myObject")
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
public MyObject createMyObject(@RequestPart("form") @Valid MyObject myObject, @RequestPart("file") @Valid MultipartFile file) {
    return repo.save(myObject);
}

In FileValidator.java:

public boolean supports(Class<?> aClass) {
    return MultipartFile.class.isAssignableFrom(aClass);
}
Mahozad
  • 18,032
  • 13
  • 118
  • 133
Magnus
  • 589
  • 8
  • 26

3 Answers3

1

I believe that the problem is related to the variadic Multipart... parameter. In the supports method of the validator you are checking for array of Multipart but I suspect that's not the correct way. Just as a trial, I would make the Multipart a single object parameter (and change the validator accordingly) to test if it works in this way.

NiVeR
  • 9,644
  • 4
  • 30
  • 35
  • I tried this now, but it made no difference. I appended my question with your suggested edits. Just for the record, my `supports` method in `FileValidator` does not only check for an array, but for a single `MultipartFile` object as well. If the input is either of those two types, the value will get accepted. Either way, my `supports` method is never even called. The breakpoint inside that method is never triggered. – Magnus Jul 17 '18 at 12:37
  • Ok, second idea: create a wrapper class which includes the `Multipart` as a property, and implement the validator on that other class instead (passing it as a parameter to the controller as well). – NiVeR Jul 17 '18 at 12:45
  • It seems complicated to craft an HTTP request that my Spring controller will accept and convert into the wrapper class. Is this something I should use as a solution, or only for diagnosis? Is there some easy way to do this? On a side note, I just edited my question with an ugly workaround that does work, sort of, but it is un-Spring-like. This workaround also reinforces my hypothesis that nothing is wrong with the `FileValidator` itself, but only the binding. – Magnus Jul 17 '18 at 13:03
  • https://stackoverflow.com/questions/7161215/spring-multipartfile-validation-and-conversion – NiVeR Jul 17 '18 at 13:07
0

@Magnus I think you have to add annotation in all the Validator Class eg.

@Component
public class MyObjectValidator extends AbstractValidator {

    public boolean supports(Class<?> aClass) { //This method is triggered.
        return (MyObject.class.equals(aClass));
    }

    public void validate(Object o, Errors e) {
        //Several validation methods goes here.
    }
}
hardik beladiya
  • 268
  • 2
  • 12
  • Finished testing it. I first set the `@Component` in my `FileValidator` and `MyObjectValidator`, and then I set it in my `AbstractValidator` as well. No effect. Then I tried removing the `@Component` from `FileValidator` and `MyObjectValidator`, but left it in `AbstractValidator`. It still did not make any difference. – Magnus Jul 17 '18 at 12:09
  • Magnus I think you forgot to add the @Controller in Controller class can you do it once if that works for you. – hardik beladiya Jul 17 '18 at 12:12
  • I accidentally omitted it from the question, but the @Controller annotation is there. See my edit. As mentioned, this is a functioning controller, except that the `FileValidator` class is never invoked. – Magnus Jul 17 '18 at 12:22
0

Maybe not actual, but you should add @Validated at class level:

@RequestMapping("/api")
@RestController
@Validated
public class Controller {

    private MyObjectRepository repo;
    private MyObjectValidator myObjectValidator;
    private FileValidator fileValidator;

    //rest of your code here
}

And you don't need @Valid at method attributes level anymore.

Timo
  • 1
  • 2