spring.svg

【Spring Boot】バリデーション

SpringBoot

パッケージの読み込み

Spring Boot のバージョンによっては、バリデーション用のパッケージがデフォルトでプロジェクトに含まれていない場合があります。

Gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'

バリデーション定義

リクエストパラメーター用に作成したクラス(DTO)のフィールドに、アノテーションを付与することでバリデーションを定義します。

public class SampleDataParam {

  @NotBlank
  @Pattern(regexp="^[0-9]{6}$")
  private String id;

  @NotBlank
  @Length(max=32)
  private String firstName;

  @NotBlank
  @Length(max=32)
  private String lastName;

  @NotNull
  @PositiveOrZero
  private int age;

}

バリデーション用のアノテーションには以下のものがあります。

アノテーション説明
@NotNullnullでないことを検証する。
@Nullnullであることを検証する。
@NotEmpty文字数または配列の要素数が 0 でないことを検証する。
@NotBlank文字列がnullでないかつ空文字でないことを検証する。
@Size(min=, max= )文字数または配列の要素数が範囲内であることを検証する。
@Length(min=, max= )文字数が範囲内であることを検証する。
@Max()整数が指定値以下であることを検証する。
@Min()整数が指定値以上であることを検証する。
@Range(min=, max= )整数が範囲内であることを検証する。
@Positive数値が正であることを検証する。
@PositiveOrZero数値が正か 0 であることを検証する。
@Negative数値が負であることを検証する
@NegativeOrZero数値が負か 0 であることを検証する。
@DecimalMax数値が指定値以下であることを検証する。
@DecimalMin数値が指定値以上であることを検証する。
@Digits(integer= ,fraction= )整数部(integer)と小数部(fraction)が指定した桁数以内であることを検証する。
@AssertFalsefalseであることを検証する。
@AssertTruetrueであることを検証する。
@Future日付が未来であることを検証する。
@FutureOrPresent日付が未来であるか今日であるかを検証する。
@Past日付が過去であることを検証する。
@PastOrPresent日付が過去であるか今日であるかを検証する。
@URL文字列が正しい URL(RFC2396)であることを検証する。
@Email文字列が正しいメールアドレス(RFC2822)であることを検証する。
@CreditCardNumber文字列が正しいクレジットカード番号であることを検証する。
@Pattern(regexp= )文字列が正規表現にマッチすることを検証する。
@Validネストしたクラスのバリデーションを実行する。

バリデーションの実行

バリデーションは@Validatedを付与することで実行できますが、どのようにパラメーターを指定したかで少し指定方法や挙動が異なります。

DTO

application/x-www-form-urlencodedのパラメーターを DTO として受け取る場合、DTO に@Validatedを付与します。

@PostMapping
public void create(@Validated SampleDataParam param) {
  //
}

バリデーションエラーが発生した場合は、BindExceptionがスローされます。

@ModelAttribute

クエリパラメーターを DTO として受け取る場合、同じく DTO に@Validatedを付与します。

@GetMapping
public SampleData get(@ModelAttribute @Validated SampleDataParam param) {
  //...
}

@RequestBody

application/jsonとしてパラメーターを受け取る場合も、同じく DTO に@Validatedを付与します。

@PostMapping
public void create(@RequestBody @Validated SampleDataParam param) {
  //
}

バリデーションエラーが発生した場合は、MethodArgumentNotValidExceptionがスローされます。

@RequestParam、@PathValiable

@RequestParam@PathValiableでは、各変数にバリデーション用のアノテーションを付与します。

@GetMapping
public SampleData getOne(@RequestParam @Pattern(regexp="^[0-9]{6}$") String id) {
  //
}

このバリデーションを実行するためには、クラスに@Validatedを付与します。

@RestController
@Validated
public class SampleController {
  @GetMapping
  public SampleData getOne(@RequestParam @Pattern(regexp="^[0-9]{6}$") String id) {
    //
  }
}

バリデーションエラーが発生した場合は、ConstraintViolationExceptionがスローされます。

結果の取得

バリデーションエラーが発生した場合に、例外をスローせずに結果を参照するにはBindingResultを引数に指定します。

@PostMapping
public void create(@RequestBody @Validated SampleDataParam param, BindingResult result) {
  if (result.hasError()) {
    //エラー処理
  }
}

MethodArgumentNotValidExceptionなどのバリデーションエラーに関する例外は、内部的にBindingResultを持っています。

BindingResultgetFieldErrors()により、フィールド名とエラーメッセージのセットを取得できます。

for (FieldError error : result.getFieldErrors()) {
  String field = error.getField();
  String message = error.getDefaultMessage();
}

List のバリデーション

List<>のバリデーションを実行したい場合は、以下のように@Validを付与し、ジェネリクスに要素ごとに実行したいバリデーションを付与します。

@NotEmpty
@Valid
private List<@NotBlank String> list;

グループ化

上記までは、設定したバリデーションはすべて実行されていました。 しかし、実際はリクエストによって実行したくないバリデーションがあったりします。

グループ化は、グループを定義することによって、実行するバリデーションを制御することができます。

以下に簡単な例を記載します。

public class SampleDataParam {

  //グループを作成
  public static interface CreateSampleData {}
  public static interface UpdateSampleData {}

  //グループを設定
  @NotBlank(groups= { UpdateSampleData.class })
  @Pattern(regexp="^[0-9]{6}$", groups= { UpdateSampleData.class })
  private String id;

  @NotBlank(groups = { CreateSampleData.class, UpdateSampleData.class, Default.class })
  @Length(max=32, groups = { CreateSampleData.class, UpdateSampleData.class, Default.class })
  private String firstName;

  @NotBlank(groups = { CreateSampleData.class, UpdateSampleData.class })
  @Length(max=32, groups = { CreateSampleData.class, UpdateSampleData.class })
  private String lastName;

  @NotNull
  @PositiveOrZero
  private int age;
}

上記は、登録用と更新用のグループとして、CreateSampleDataUpdateSampleDataを作成しています。 見ての通り、グループはinterfaceとして定義します。

バリデーションの実行制御として、groupsプロパティにどのグループの時にバリデーションを実行するかを設定します。 上記の場合、以下のように実行制御が行われます。

DefaultCreateSampleDataUpdateSampleData
id--
firstName
lastName-
age--

Default.classは、グループの指定がない場合のグループです。 上記では、グループの指定がないageと、直接Default.classを指定しているfirstNameが対象となります。

グループによるバリデーションの実行制御は、@Validatedのプロパティに実行したいグループを指定することで行います。

@RestController
public class SampleController {
  @PostMapping
  public void create(@RequestBody @Validated({ CreateSampleData.class}) SampleDataParam param) {
    //
  }
  @PutMapping
  public void update(@RequestBody @Validated({ UpdateSampleData.class }) SampleDataParam param) {
    //
  }
}

POST の場合は、CreateSampleDataを指定しているfirstNamelastNameのバリデーションが実行されます。 PUT の場合は、UpdateSampleDataを指定しているidfirstNamelastNameのバリデーションが実行されます。

このように、グループ化を行うことで実行するバリデーションの制御を行うことができます。

では、具体的な例として更新時のみidのバリデーションを実行し、登録時は行わないようにするにはどうしたらよいでしょうか。

いくつかやり方があるとは思いますが、ここでは 2 つ紹介します。

1 つは、@ValidatedDefault.classを指定する方法です。

public class SampleDataParam {

  //グループを作成
  public static interface UpdateSampleData {}

  //グループを設定
  @NotBlank(groups= { UpdateSampleData.class })
  @Pattern(regexp="^[0-9]{6}$", groups= { UpdateSampleData.class })
  private String id;

  @NotBlank
  @Length
  private String firstName;

  @NotBlank
  @Length
  private String lastName;

  @NotNull
  @PositiveOrZero
  private int age;
}
@RestController
public class SampleController {
  @PostMapping
  public void create(@RequestBody @Validated SampleDataParam param) {
    //
  }
  @PutMapping
  public void update(@RequestBody @Validated({ UpdateSampleData.class, Default.class }) SampleDataParam param) {
    //
  }
}

このようにすれば、登録時はグループを設定していないid以外のバリデーションが実行され、更新時はすべてのバリデーションが実行されます。

もう 1 つは、Defaultを継承したグループを作成する方法です。

public class SampleDataParam {

  //グループを作成
  public static interface UpdateSampleData extends Default {}

  //グループを設定
  @NotBlank(groups= { UpdateSampleData.class })
  @Pattern(regexp="^[0-9]{6}$", groups= { UpdateSampleData.class })
  private String id;

  @NotBlank
  @Length
  private String firstName;

  @NotBlank
  @Length
  private String lastName;

  @NotNull
  @PositiveOrZero
  private int age;
}
@RestController
public class SampleController {
  @PostMapping
  public void create(@RequestBody @Validated SampleDataParam param) {
    //
  }
  @PutMapping
  public void update(@RequestBody @Validated({ UpdateSampleData.class }) SampleDataParam param) {
    //
  }
}

Defaultを継承したグループを指定した場合、グループの指定がないバリデーションも一緒に実行されるようになります。

カスタムバリデーション

例として、文字列が指定したフォーマットの日付であることを検証するFormatDateを作成します。

@FormatDate(format="uuuuMMdd")
private String date;

アノテーション実装

@FormatDateアノテーションを以下のように実装します。

@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {FormatDateValidator.class})
public @interface FormatDate {
  String message() default "Invalid value";
  String format();
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {}
}

詳細は割愛し、必要な部分のみ説明します。

プロパティ

アノテーションは、@interface アノテーション名として作成することができます。 @interface内には、引数として指定するプロパティを定義します。

message()にはエラーメッセージを設定し、groups()payload()はとりあえずこの形で書くように覚えておきます。 この 3 つのプロパティは必ず設定してください。

また今回はformatというプロパティを使用したいのでformat()を追加しています。 @FormatDate("uuuu/MM/dd")のようにプロパティ名を省略したい場合は、value()としてプロパティを追加します。

アノテーション

@Targetには、アノテーションを付与する対象を設定します。

  • ElementType.FIELD:クラスのフィールド
  • ElementType.PARAMETER:メソッドのパラメータ―
  • ElementType.TYPE_USE:型(List<>のジェネリクス用)

@Constraintには、バリデーションの処理を実装したクラスを指定します。 今回はFormatDateValidatorとして実装します。

バリデーション実装

バリデーションの処理は、以下のようにConstraintValidatorを実装したクラスを作成します。

public class FormatDateValidator implements ConstraintValidator<FormatDate, String> {

  private String format;

  @Override
  public void initialize(FormatDate formatDate) {
    format = formatDate.format();
  }

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    try {
      DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format)
                                                     .withResolverStyle(ResolverStyle.STRICT);
      LocalDate.parse(value, formatter);
      return true;
    } catch(DateTimeException e) {
      return false;
    }
  }
}

ConstraintValidatorのジェネリクスには、アノテーションとバリデーションの対象となる値の型を指定します。 今回は@FormatDateで文字列のバリデーションを行うため、<FormatDate, String>となります。

ConstraintValidatorでは、initializeisValidの 2 つのメソッドを実装します。

initializeは、パラメーターの値を取得するための初期化処理です。 パラメータ―がない場合は実装不要です。

isValidは、具体的なバリデーションの処理を定義します。 こちらのメソッドの実装は必須となります。

メッセージの設定

ValidationMessage.properties

バリデーションのメッセージは、messageプロパティを指定することで変更できます。

@NotNull(message = "値はNullです。")

しかしながら、フィールド 1 つ 1 つにメッセージを設定するのはかなり手間です。

そこでValidationMessages.propertiesというファイルをsrc/resources直下に作成します。 以下のようにバリデーションごとにメッセージを設定することが可能です。

javax.validation.constraints.NotNull.message=値はNullです。
javax.validation.constraints.NotBlank.message=値はNullまたは空文字です。

カスタムバリデーションも同様に設定することが可能です。 対応するアノテーションのmessages()を以下のように設定します。

@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {FormatDateValidator.class})
public @interface FormatDate {
  String message() default "{custom.validation.FormatDate.message}";
  String format();
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {}
}

あとは設定したプロパティと同じものをValidationMessages.propertiesに設定するだけです。

custom.validation.FormatDate.message=指定したフォーマットの日付ではありません。

UTF-8 への対応

プロジェクトが UTF-8 の場合は、そのままだと文字化けしてしまいます。 対策として、ValidationMessage.propertiesを UTF-8 にエンコードし、以下のように読み込む処理を定義します。

@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Bean
  public MessageSource messageSource() {
    ReloadableResourceBundleMessageSource messageSource
      = new ReloadableResourceBundleMessageSource();

    messageSource.setBasename("classpath:ValidationMessages");
    messageSource.setDefaultEncoding("UTF-8");
    return messageSource;
  }

  @Bean
  public LocalValidatorFactoryBean validator() {
    LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
    bean.setValidationMessageSource(messageSource());
    return bean;
  }

  @Override
  public Validator getValidator() {
    return validator();
  }
}

プロパティの指定方法

メッセージにプロパティを使用したい場合は、{プロパティ名}のように指定します。

custom.validation.FormatDate.message={format}の日付ではありません。