Home > other >  Is it okay to put business(custom) validate in the DTO?
Is it okay to put business(custom) validate in the DTO?

Time:04-15

@Getter
public class PolicyRequestDto {

    @Getter
    @Builder
    @AllArgsConstructor
    public static class InsertPolicyRequest {
        @NotNull(message = "need typeNo")
        private Integer typeNo;
        private String typeDescription;
        private Long volume;
        private Integer retryCnt;
        private Integer authTime;

        public InsertPolicyRequest insertValidate() {

            PolicyTypeEnum policyEnumList = new PolicyTypeEnum();
            Stream<Integer> volumeList = policyEnumList.getVolumeList().stream();
            Stream<Integer> authTimeList = policyEnumList.getAuthTimeList().stream();
            Stream<Integer> retryCntList = policyEnumList.getRetryCntList().stream();

            switch (Objects.requireNonNull(typeNo, "no typeNo")) {
                case 0 :
                    // case 0
                    break;
                case 1 :
                    // case 1
                    break;
                case 2 :
                    // case 2
                    break;
                default : throw new IllegalArgumentException("Error");
            }

            return this;
        }

        public PolicyEntity toEntity () {
            return PolicyEntity.builder()
                    .typeNo(typeNo)
                    .volume(volume)
                    .retryCnt(retryCnt)
                    .authTime(authTime)
                    .build();
        }
    }
}
public PolicyResponseDto.CommonResponse addWifiServicePolicy(@Valid PolicyRequestDto.InsertPolicyRequest insertRequest) {

        insertRequest.insertValidate();
        PolicyEntity policyEntity = insertRequest.toEntity();

        ...

        return ...
}

If there is a validation required by that service that cannot be done with the @Validated annotation, can it be done in the DTO?

Right now, the verification process is at a level that varies depending on the input parameters, for example, code. The service wants to perform business logic only with data that has passed this verification. It's annoying that validation code is put at the top of each method, but I think it's necessary code.

Is there any problem with doing this in the DTO?

p.s I want to make a service object independent of the controller, so that there is no problem even if only the service is separated.

CodePudding user response:

You can create your custom validation annotation.

@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyConstraintValidator.class)
public @interface MyConstraint {

    String message() default "default error message";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

This annotation works on the field or method level. The validator must implement ConstraintValidator.

public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Integer> {

    @Override
    public boolean isValid(Integer typeNo, ConstraintValidatorContext context) {
        PolicyTypeEnum policyEnumList = new PolicyTypeEnum();
        Stream<Integer> volumeList = policyEnumList.getVolumeList().stream();
        Stream<Integer> authTimeList = policyEnumList.getAuthTimeList().stream();
        Stream<Integer> retryCntList = policyEnumList.getRetryCntList().stream();

        switch (Objects.requireNonNull(typeNo, "no typeNo")) {
            case 0 :
                // case 0
                break;
            case 1 :
                // case 1
                break;
            case 2 :
                // case 2
                break;
            default :
                return false;
        }
        return true;
    }
}

If you need to access data from other fields, you need to update the annotation to be applicable on the type level - @Target( { ElementType.TYPE }), and accordingly change the validator to work with the type - InsertPolicyRequest:

public class MyConstraintValidator implements ConstraintValidator<MyConstraint, InsertPolicyRequest> {

    @Override
    public boolean isValid(InsertPolicyRequest request, ConstraintValidatorContext context) {
        PolicyTypeEnum policyEnumList = new PolicyTypeEnum();
        Stream<Integer> volumeList = policyEnumList.getVolumeList().stream();
        Stream<Integer> authTimeList = policyEnumList.getAuthTimeList().stream();
        Stream<Integer> retryCntList = policyEnumList.getRetryCntList().stream();

        switch (Objects.requireNonNull(request.getTypeNo(), "no typeNo")) {
            case 0 :
                // case 0
                break;
            case 1 :
                // case 1
                break;
            case 2 :
                // case 2
                break;
            default :
                return false;
        }
        return true;
    }
}

Check this guide as well.

Edit: It's doable with a single annotation. Have an interface which will take care of extracting common data, let's call it PolicyRequestData.

public interface PolicyRequestData {

  Integer getTypeNo();
  String getTypeName();
}

Then change the annotation from above to work on the type level and rename it to MyCommonConstraint. Update ConstraintValidator to work with PolicyRequestData instead of concrete class.

public class MyCommonConstraintValidator implements ConstraintValidator<MyCommonConstraint, PolicyRequestData> {

  @Override
  public boolean isValid(PolicyRequestData request, ConstraintValidatorContext constraintValidatorContext) {
    PolicyTypeEnum policyEnumList = new PolicyTypeEnum();
    Stream<Integer> volumeList = policyEnumList.getVolumeList().stream();
    Stream<Integer> authTimeList = policyEnumList.getAuthTimeList().stream();
    Stream<Integer> retryCntList = policyEnumList.getRetryCntList().stream();

    switch (Objects.requireNonNull(request.getTypeNo(), "no typeNo")) {
      case 0 :
        // case 0
        break;
      case 1 :
        // case 1
        break;
      case 2 :
        // case 2
        break;
      default :
        return false;
    }
    return true;
  }
}

Like this the annotation will work on any class implementing PolicyRequestData.

@MyCommonConstraint
public class InsertPolicyRequest implements PolicyRequestData {

  private Integer typeNo;
  private String typeName;

  @Override
  public Integer getTypeNo() {
    return this.typeNo;
  }

  @Override
  public String getTypeName() {
    return this.typeName;
  }

  //setters, if needed
}

UpdatePolicyRequest is absolutely identical, except for class name.

If you need to validate classes with specific data extend the interface.

public interface SpecificPolicyRequestData extends PolicyRequestData {

  String getOtherData();
}

Have the specific class implement it and create annotation to work with this interface. Then apply both annotations on the class(if both validations are needed):

@MyCommonConstraint
@MySpecificConstraint
public class SomeSpecificPolicyRequest implements SpecificPolicyRequestData {

  @Override
  public Integer getTypeNo() {
    return null;
  }

  @Override
  public String getTypeName() {
    return null;
  }

  @Override
  public String getOtherData() {
    return null;
  }
}
  • Related