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