【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}の日付ではありません。