JSR303の@ValidとSpringBootの@Validatedの違い

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

Javaには、JSR303 Bean Validationという私の好きなValidation仕様があります。@Validをつければ、クラスのフィールドに@Empty@Min(1)のようなアノテーションをつけるだけで、値のバリデーションが可能です。Springにはこれを拡張した@Validatedがあります。この使い方について軽く触れます。

@Validはなに?

これはJavaの標準仕様で、クラスのフィールドにアノテーションをつけて、所定メソッドを実行するとバリデーションを行ってくれます。

@Data
class Form {
   @NotEmpty
   private String name;
 }


Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); // validatorのインスタンス生成
Form form = new Form(); // データクラスのインスタンス
Set<ConstraintViolation<Form>> validate = validator.validate(form); // バリデーションの実行

フィールドに付けられたアノテーションの情報を元にバリデーションをしてくれます。フィールドの仕様とバリデーションがセットになっているので視認性もよく使い勝手はいいです。ただ、使い勝手が良すぎるせいで、ある程度約束を守ってもらわないと設定がバラバラになってしまうという問題もあります。

@Validatedはなに?

カンタンにいうと、@Validを拡張したものが、SpringFramework内にあります。拡張の主なポイントはグループの指定です。

グループの指定とは?

とあるユースケースで、バリデーションを指定したいフィールドを絞りたいことがあるかと思います。1つのデータクラスにリクエストパラメータを入れるのですが、入力フォームが多段になっていたり、簡易フォームと詳細フォームでわかれていたりとあると思います。そういうユースケースに効果を発揮します。

実装

下記のような実装があります。simpleのエンドポイントは、 idnameは必須、detailの方のエンドポイントは、simpleのエンドポイントに加えて、agenickNameが必須になります。groupsという属性を定義することによって、バリデーションをどこまで行うかの識別を行っています。

@RestController
@RequestMapping("valid")
public class ValidController {

    @GetMapping("simple")
    public Form simple(@Validated(value = {SimpleForm.class}) Form form) {
        return form;
    }

    @GetMapping("detail")
    public Form detail(@Validated(value = {SimpleForm.class, DetailForm.class}) Form form) {
        return form;
    }

    @Data
    static class Form {
        @NotNull(groups = {SimpleForm.class})
        private Integer id;
        @NotBlank(groups = {SimpleForm.class})
        private String name;

        @NotNull(groups = {DetailForm.class})
        private Integer age;
        @NotBlank(groups = {DetailForm.class})
        private String nickName;

    }
}

simpleのエンドポイント

@Validated(value = {SimpleForm.class}) Form form が引数に定義されることによって、Formクラスのフィールドにgroups = {SimpleForm.class}アノテーションが付与されているところだけバリデーションが実行されます。

$ curl "http://localhost:8080/valid/simple?id=1&name=sample"

{"id":1,"name":"sample","age":null,"nickName":null}

detailのエンドポイント

simpleのエンドポイントをdetailに変えて、クエリパラメータはそのままに実行してみます。

$ curl "http://localhost:8080/valid/detail?id=1&name=sample"

{"timestamp":"2021-04-29T15:17:56.903+00:00","status":400,"error":"Bad Request","trace":"org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errors\nField error in object 'form' on field 'age': rejected value [null]; codes [NotNull.form.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [form.age,age]; arguments []; default message [age]]; default message [null は許可されていません] ....

上記のようにエラーがバリデーションエラーが発生しています。これは先程のsimpleのエンドポイントとは異なるバリデーション設定がされています。@Validated(value = {SimpleForm.class, DetailForm.class}) Form formのバリデーション設定で、DetailForm.classが追加されています。こちらが追加されている為、 Formクラスに定義してあるとおり、agenickName の定義が必須になっています。

さいごに

@Validatedについてカンタンに説明させていただきました。気をつけねばならないのが、 groupsをつけると@Validは動作しなくなるというのがあります。ただ、@ValidatedはSpring依存になってしまうので、早いところ共通仕様をだしてほしいところであります