3

I have a DTO that looks something like this:

class VehicleDto {
    private String type;
    private Car car;
    private Bike bike;
}

Now depending on the type, I need to validate on at least one of Car and Bike.

Both cannot be present in the same request.

How can I do that?

nirvair
  • 4,001
  • 10
  • 51
  • 85
  • You can write 2 custom validators for Car and Bike that take the type into account. Or one on class level. – Nico Van Belle Sep 19 '17 at 11:25
  • 3
    if both cannot be present in the same request why the VehicleDTO contains both? It would be better to replace both car and bike fields with one vehicle type (abstract class extended by car and bike) – pezetem Sep 19 '17 at 11:47
  • Can you write a sample code for that? I understand what you said, but still not sure if I am correct. – nirvair Sep 19 '17 at 15:55
  • @pezetem asked another question here: https://stackoverflow.com/questions/46310281/design-of-multiple-child-dtos-into-a-single-request-spring-boot – nirvair Sep 19 '17 at 21:53

2 Answers2

6

Having two fields in class, while only one of them can present, seems like a design smell for me. But if you insist on such design - you can create a custom Validator for your VehicleDto class.

public class VehicleValidator implements Validator {

    public boolean supports(Class clazz) {
        return VehicleDto.class.equals(clazz);
    }

    public void validate(Object obj, Errors errors) {

        VehicleDto dto = (VehicleDto) obj;

        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "type",
                                    "error.message.for.type.field");

        if (null != dto.getType()
              && null != dto.getCar()
              && null != dto.getBike()) {
            switch(dto.getType()) {
                case "car":
                    errors.rejectValue("bike", "error.message.for.bike.field");
                    break;
                case "bike":
                    errors.rejectValue("car", "error.message.for.car.field");
                    break;
            }
        }
    }
}

Also, see Spring documentation about validation:

Anatoly Shamov
  • 2,608
  • 1
  • 17
  • 27
  • What should be an ideal design for such cases? – nirvair Sep 19 '17 at 15:54
  • Added a new question for the design issue: https://stackoverflow.com/questions/46310281/design-of-multiple-child-dtos-into-a-single-request-spring-boot – nirvair Sep 19 '17 at 21:54
0

For example, if we want to check whether my TaskDTO object is valid, by comparing its two attributes dueDate and repeatUntil , following are the steps to achieve it.

dependency in pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

DTO class:

@ValidTaskDTO
public class TaskDTO {

    @FutureOrPresent
    private ZonedDateTime dueDate;

    @NotBlank(message = "Title cannot be null or blank")
    private String title;

    private String description;

    @NotNull
    private RecurrenceType recurrenceType;

    @Future
    private ZonedDateTime repeatUntil;

}

Custom Annotation:

@Constraint(validatedBy = {TaskDTOValidator.class})
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidTaskDTO {
    String message() default "Due date should not be greater than or equal to Repeat Until Date.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Constraint Validator:

public class TaskDTOValidator implements ConstraintValidator<ValidTaskDTO, TaskDTO> {
    @Override
    public void initialize(ValidTaskDTO constraintAnnotation) {

    }

    @Override
    public boolean isValid(TaskDTO taskDTO, ConstraintValidatorContext constraintValidatorContext) {
        if (taskDTO.getRecurrenceType() == RecurrenceType.NONE) {
            return true;
        }

        return taskDTO.getRepeatUntil() != null && taskDTO.getDueDate().isBefore(taskDTO.getRepeatUntil());
    }
}

Make sure that you have @Valid in front of RequestBody of a postmapping method in your RestController. Only then the validation will get invoked:

@PostMapping
public TaskReadDTO createTask(@Valid @RequestBody TaskDTO taskDTO) {
.....
}

I hope this helps. If you need a detailed explanation on steps, have a look at this video