1

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.

Paul Marcelin Bejan
  • 990
  • 2
  • 7
  • 14
  • See this answer https://stackoverflow.com/questions/28150405/validation-of-a-list-of-objects-in-spring the same problem is described there. Seems nothing changed since then and the spring controller still has this issue with collection validation. – mark_o May 23 '23 at 12:00
  • @mark_o it is different, on the question you mention, he is only applying one validation, instead, I need to apply different validations based on validation group, so a little bit more complex. – Paul Marcelin Bejan May 23 '23 at 13:27

1 Answers1

0

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);
    
    }

}
Paul Marcelin Bejan
  • 990
  • 2
  • 7
  • 14