Search code examples
javaspring-mvcbean-validationspring-restcontroller

How to validate objects of collection on Controller with Validated?


I have the following DTO that I want to validate:

@Data
public class ContinentDto {

    @Null(groups = { CreateValidation.class }, message = "ID must be null")
    @NotNull(groups = { UpdateValidation.class }, message = "ID must not be null")
    @JsonProperty
    private Integer id;

    @NotBlank(groups = { CreateValidation.class, UpdateValidation.class }, message = "continentName must not be blank")
    @JsonProperty
    private String continentName;

    public interface CreateValidation {
        // validation group marker interface
    }

    public interface UpdateValidation {
        // validation group marker interface
    }

}

It contains two Validation Groups: CreateValidation and UpdateValidation with different validations to be applied.

I'm perfectly able to validate the DTO when it is passed as a single argument, but for the API that has a collection of DTO as a request, the validation is not applied anymore.

This is my controller:

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/continent")
public class ContinentRestController {

    private final ContinentService service;

    @PostMapping(value = "/save-one", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public @ResponseBody Integer save(
           @Validated(ContinentDto.CreateValidation.class) 
           @RequestBody final ContinentDto dto) throws TechnicalException {

        return service.save(dto);

    }

    @PostMapping(value = "/save-all", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public @ResponseBody Collection<Integer> saveAll(
           @Validated(ContinentDto.CreateValidation.class) 
           @RequestBody final Collection<ContinentDto> dtos) throws TechnicalException {

        return service.save(dtos);

    }

    @PutMapping(value = "/update-one", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public @ResponseBody Integer update(
           @Validated(ContinentDto.UpdateValidation.class) 
           @RequestBody final ContinentDto dto) throws FunctionalException, TechnicalException {
    
        return service.update(dto);

    }

    @PutMapping(value = "/update-all", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public @ResponseBody Collection<Integer> updateAll(
           @Validated(ContinentDto.UpdateValidation.class)
           @RequestBody final Collection<ContinentDto> dtos) throws FunctionalException, TechnicalException {
    
        return service.update(dtos);
    
    }

}

I have also tried to add the @Valid annotation inside the diamond brackets of the collection but nothing changed.

I see on other questions about list validation that the suggested answer was to apply the @Validated annotation at the class level, but in my case, I think it is not possible because I'm using Validation Groups.


Solution

  • I'm still looking to find a solution using annotation but until then, I solved it in that way:

    I have created a ValidatorUtils

    @NoArgsConstructor(access = AccessLevel.PRIVATE)
    public class ValidatorUtils {
    
        public static <TO_VALIDATE> void validate(Collection<TO_VALIDATE> collectionToValidate) {
            Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
            for (TO_VALIDATE toValidate : collectionToValidate) {
                Set<ConstraintViolation<TO_VALIDATE>> violations = validator.validate(toValidate);
                if (!violations.isEmpty()) {
                    throw new ConstraintViolationException(violations);
                }
            }
        }
    
        public static <TO_VALIDATE> void validateGroups(Collection<TO_VALIDATE> collectionToValidate, Class<?>... groups) {
            Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
            for (TO_VALIDATE toValidate : collectionToValidate) {
                Set<ConstraintViolation<TO_VALIDATE>> violations = validator.validate(toValidate, groups);
                if (!violations.isEmpty()) {
                    throw new ConstraintViolationException(violations);
                }
            }
        }
    
    }
    

    and then I'm using it in the controller before calling the service:

    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/api/continent")
    public class ContinentRestController {
    
        private final ContinentService service;
    
        @PostMapping(value = "/save-one", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
        public @ResponseBody Integer save(
               @Validated(ContinentDto.CreateValidation.class) 
               @RequestBody final ContinentDto dto) throws TechnicalException {
    
            return service.save(dto);
    
        }
    
        @PostMapping(value = "/save-all", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
        public @ResponseBody Collection<Integer> saveAll(
               @RequestBody final Collection<ContinentDto> dtos) throws TechnicalException {
    
            ValidatorUtils.validateGroups(dtos, ContinentDto.CreateValidation.class);
            return service.save(dtos);
    
        }
    
        @PutMapping(value = "/update-one", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
        public @ResponseBody Integer update(
               @Validated(ContinentDto.UpdateValidation.class) 
               @RequestBody final ContinentDto dto) throws FunctionalException, TechnicalException {
        
            return service.update(dto);
    
        }
    
        @PutMapping(value = "/update-all", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
        public @ResponseBody Collection<Integer> updateAll(
               @RequestBody final Collection<ContinentDto> dtos) throws FunctionalException, TechnicalException {
        
            ValidatorUtils.validateGroups(dtos, ContinentDto.UpdateValidation.class);
            return service.update(dtos);
        
        }
    
    }