Search code examples
spring-bootvalidationspring-annotationsspring-validatormultipartfile

In SpringBoot, how do I create a custom validator for a MultipartFile parameter?


I'm using Spring Boot 2.4. I have the following controller with a method that accepts a MultipartFile object.

@RestController
public class MyController extends AbstractController

    ...
  @Override
  public ResponseEntity<ResponseData> add(
    ...
      @Parameter(description = "file detail") @Validated @RequestPart("myFile")
          MultipartFile myFile,
    ...
    ) {

I would like to validate that this MultipartFile contains the data that I want (e.g. is of a particular mime type). So I have written the below validator ...

@Documented
@Constraint(validatedBy = MultipartFileValidator.class)
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MultipartFileConstraint {
  String message() default "Incorrect file type.";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};
}

and its implementation class ...

public class MultipartFileValidator
    implements ConstraintValidator<MultipartFileConstraint, MultipartFile> {

  @Override
  public void initialize(final MultipartFileConstraint constraintAnnotation) {
    log.info("\n\n\n\nconstructor called\n\n\n\n");
  }

  @Override
  public boolean isValid(
      MultipartFile file, ConstraintValidatorContext constraintValidatorContext) {
    log.info("Validating file");
    ...
  }
}

However, when I invoke my endpoint, I don't see that my validator is called (for one, the log statement is never printed nor breakpoints hit). What else do I need to do to register my validator for this MultipartFile param?


Solution

  • As per the Spring Documentation:

    Can also be used with method level validation, indicating that a specific class is supposed to be validated at the method level (acting as a pointcut for the corresponding validation interceptor), but also optionally specifying the validation groups for method-level validation in the annotated class.

    Applying this annotation at the method level allows for overriding the validation groups for a specific method but does not serve as a pointcut; a class-level annotation is nevertheless necessary to trigger method validation for a specific bean to begin with. Can also be used as a meta-annotation on a custom stereotype annotation or a custom group-specific validated annotation.

    So, here we have to keep in mind what are the placement of @Validated and validator annotation. Code:

    Controller class : @Validated added at class level and @ValidFile (Custom validator annotation) in the method

    @RestController
    @Validated
    @Slf4j
    public  class MyController {
    
        @RequestMapping("/add")
        public ResponseEntity<ResponseData> add(@ValidFile @RequestParam("file")  MultipartFile file) {
    
            log.info("File Validated");
            return  ResponseEntity.status(HttpStatus.OK).body(new ResponseData("Valid file received"));
        }
    }
    

    Validator Annotation

    @Documented
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = {FileValidator.class})
    public @interface ValidFile {
        Class<? extends Payload> [] payload() default{};
        Class<?>[] groups() default {};
        String message() default "Only pdf,xml,jpeg,jpg files are allowed";
    }
    

    Validator class

    @Slf4j
    public class FileValidator implements ConstraintValidator<ValidFile, MultipartFile> {
    
        @Override
        public void initialize(ValidFile validFile) {
            log.info("File validator initialized!!");
        }
    
        @Override
        public boolean isValid(MultipartFile multipartFile,
                               ConstraintValidatorContext   constraintValidatorContext) {
            log.info("Validating file");
            String contentType = multipartFile.getContentType();
            assert contentType != null;
            return isSupportedContentType(contentType);
        }
        private boolean isSupportedContentType(String contentType) {
            return contentType.equals("application/pdf")
                    || contentType.equals("text/xml")
                    || contentType.equals("image/jpg")
                    || contentType.equals("image/jpeg");
        }
    }
    

    Output : Success:

    {
        "message": "Valid file received"
    }
    

    Exception handler

     @ExceptionHandler(ConstraintViolationException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
            return new ResponseEntity<>("Validation error: " + e.getMessage(), HttpStatus.BAD_REQUEST);
     }
    

    Failure:

    Validation error: Only pdf,xml,jpeg,jpg files are allowed