【Spring Boot】バリデーション
パッケージの読み込み
Spring Boot のバージョンによっては、バリデーション用のパッケージがデフォルトでプロジェクトに含まれていない場合があります。
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;
}バリデーション用のアノテーションには以下のものがあります。
| アノテーション | 説明 |
|---|---|
@NotNull | nullでないことを検証する。 |
@Null | nullであることを検証する。 |
@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)が指定した桁数以内であることを検証する。 |
@AssertFalse | falseであることを検証する。 |
@AssertTrue | trueであることを検証する。 |
@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を持っています。
BindingResultのgetFieldErrors()により、フィールド名とエラーメッセージのセットを取得できます。
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;
}上記は、登録用と更新用のグループとして、CreateSampleDataとUpdateSampleDataを作成しています。
見ての通り、グループはinterfaceとして定義します。
バリデーションの実行制御として、groupsプロパティにどのグループの時にバリデーションを実行するかを設定します。
上記の場合、以下のように実行制御が行われます。
| Default | CreateSampleData | UpdateSampleData | |
|---|---|---|---|
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を指定しているfirstName、lastNameのバリデーションが実行されます。
PUT の場合は、UpdateSampleDataを指定しているid、firstName、lastNameのバリデーションが実行されます。
このように、グループ化を行うことで実行するバリデーションの制御を行うことができます。
では、具体的な例として更新時のみidのバリデーションを実行し、登録時は行わないようにするにはどうしたらよいでしょうか。
いくつかやり方があるとは思いますが、ここでは 2 つ紹介します。
1 つは、@ValidatedにDefault.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では、initializeとisValidの 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}の日付ではありません。