Javaのカスタムバリデーションで2つ以上のプロパティをチェックするアノテーション

エキサイト株式会社 メディア開発の佐々木です。

現在、SpringBootで2つ以上のプロパティをチェックするカスタムバリデーションを共有します。

アノテーションの定義

下記のようにカスタムアノテーションを定義します。 (@Constraint(validatedBy = NotBlankAny.NotBlankAnyValidator.class) ここの部分はエラーになります)

@Documented
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NotBlankAny.NotBlankAnyValidator.class)
public @interface NotBlankAny {
    String message() default "please {fields} not empty.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    String[] fields();
}

バリデーション定義

次にバリデーション部分を定義します。

    class NotBlankAnyValidator implements ConstraintValidator<NotBlankAny,Object> {

        private String[] fields;

        @Override
        public void initialize(NotBlankAny constraintAnnotation) {
            this.fields = constraintAnnotation.fields();  // ※1
        }

        @Override
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            if (Objects.isNull(value)) {
                return false;
            }
            BeanWrapperImpl beanWrapper = new BeanWrapperImpl(value);
            return Stream.of(fields).allMatch(e -> StringUtils.isNotBlank((String)beanWrapper.getPropertyValue(e))); //  ※2
        }
    }

※1アノテーション内で指定されたフィールドの文字列が配列で入ってきます。※2で一つずつ取り出してチェックします。

使い方

対象のデータクラスに、 @NotBlankAny(fields = {"フィールド名1","フィールド名2"}) を定義すると、そのデータを使ってバリデーションがかかります。

@RestController
@RequestMapping
@RequiredArgsConstructor
@Slf4j
public class DemoController {

    @GetMapping
    public Mono index(@Valid Form form) {
        return Mono.defer(() -> Mono.just(form));
    }

    @Data
    @NotBlankAny(fields = {"firstName","lastName"})  // こんな感じで設定します
    static class Form {
        private String firstName;
        private String lastName;
    }
}

これでチェックができるので、再利用性はかなり高いバリデーションができると思います。

最後に

フィールド複数のバリデーションを使いたいときって結構あると思います。メールアドレスの再入力とか、パスワードを同じもの2つ入れるとか。実際にチェックのところは、一致や異なるものやどれかひとつみたいなものは、バリデーション側での実装でバリエーションは増やせると思います。

宣伝

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

www.wantedly.com

おまけ

全体のコード

@Documented
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NotBlankAny.NotBlankAnyValidator.class)
public @interface NotBlankAny {
    String message() default "One of {fields} must not be empty";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    String[] fields();

    class NotBlankAnyValidator implements ConstraintValidator<NotBlankAny,Object> {

        private String[] fields;

        @Override
        public void initialize(NotBlankAny constraintAnnotation) {
            this.fields = constraintAnnotation.fields();
        }

        @Override
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            if (Objects.isNull(value)) {
                return false;
            }
            BeanWrapperImpl beanWrapper = new BeanWrapperImpl(value);
            return Stream.of(fields).allMatch(e -> StringUtils.isNotBlank((String) beanWrapper.getPropertyValue(e)));
        }
    }
}