- Spring
Spring Bootバリデーション一覧|@Validと@Validated
Spring Bootのバリデーションとは、Jakarta Validation(旧Bean Validation)の制約アノテーションをフィールドや引数に付け、入力値を自動でチェックする仕組みです。spring-boot-starter-validation を追加し、@NotBlank や @Size などを付けた引数に @Valid か @Validated を付けると、Controllerに届いた値が検証されます。
検索上位の記事は、よく使う4〜5種のアノテーションの紹介や、javax.validation 前提の書き方が中心です。この記事では、Jakarta Validation 3.1の標準アノテーション22種すべてを「対象型・nullの扱い・例」で一覧化し、@Validと@Validatedの違いを比較表で整理します。さらに、エラーをJSONで返す @RestControllerAdvice(MethodArgumentNotValidException と HandlerMethodValidationException の両方)、日本語メッセージ、相関チェックのカスタムアノテーション、javaxからjakartaへの移行までを扱います。
対象:Spring Boot 4.1系(3.5系も同じ考え方)、Jakarta Validation 3.1、Hibernate Validator 9.0、Java 21。検証環境ではMaven Centralに接続できないためSpring本体は動かしておらず、Spring関連のコードは公式リファレンスとJavadocで確認して書いています(JSONは仕様に基づく出力例です)。外部ライブラリなしで動く部分(正規表現、文字数、メッセージファイル、カスタムバリデーターのロジック)はJava 21で実行した結果を載せています。
バリデーションアノテーションの早見表
Jakarta Validation 3.1の標準アノテーションは22種です(3.0から追加はなく、3.1ではrecordへの対応が明確になりました)。パッケージはすべて jakarta.validation.constraints です。
| アノテーション | チェック内容 | 主な対象型 | nullのとき | 例 |
|---|---|---|---|---|
@NotNull |
nullでない | すべて | エラー | @NotNull Integer quantity |
@NotEmpty |
nullでも空でもない | 文字列・Collection・Map・配列 | エラー | @NotEmpty List<String> tags |
@NotBlank |
nullでも空白だけでもない | 文字列 | エラー | @NotBlank String name |
@Null |
nullである | すべて | OK | 作成時はidを送らせない |
@Size |
文字数・要素数の範囲 | 文字列・Collection・Map・配列 | OK | @Size(min = 2, max = 50) |
@Min / @Max |
整数の下限・上限 | int・long・BigDecimal・BigInteger など | OK | @Min(1) @Max(99) |
@DecimalMin / @DecimalMax |
小数を含む下限・上限 | 数値型・数値の文字列 | OK | @DecimalMin(value = "0.0", inclusive = false) |
@Positive / @PositiveOrZero |
正の数/0以上 | 数値型 | OK | @PositiveOrZero int stock |
@Negative / @NegativeOrZero |
負の数/0以下 | 数値型 | OK | @Negative int adjustment |
@Digits |
整数部・小数部の桁数 | 数値型・数値の文字列 | OK | @Digits(integer = 6, fraction = 2) |
@Past / @PastOrPresent |
過去/過去か現在 | LocalDate・LocalDateTime・Instant など | OK | @Past LocalDate birthDate |
@Future / @FutureOrPresent |
未来/未来か現在 | 日付・日時型 | OK | @Future LocalDate dueDate |
@Pattern |
正規表現に全体一致 | 文字列 | OK | @Pattern(regexp = "[0-9]{3}-[0-9]{4}") |
@Email |
メールアドレスの形式 | 文字列 | OK | @Email String email |
@AssertTrue / @AssertFalse |
trueである/falseである | boolean・Boolean | OK | @AssertTrue boolean agreed |
nullをエラーにするのは @NotNull・@NotEmpty・@NotBlank の3つだけです。それ以外はnullを「OK」として通すため、必須かつ形式チェックをしたい項目は @NotBlank と @Pattern のように組み合わせます。
| やりたいこと | 書き方 |
|---|---|
| 入力チェックを有効にする | spring-boot-starter-validation を追加 |
| リクエストボディ(JSON)を検証 | @Valid @RequestBody CreateRequest req |
| パス変数・クエリパラメータを検証 | @PathVariable @Positive long id(引数に直接付ける) |
| 作成時と更新時でルールを変える | @Validated(OnUpdate.class) とグループ |
| 子オブジェクトやリストの中身も検証 | フィールドに @Valid、List<@NotBlank String> |
| Serviceのメソッド引数を検証 | クラスに @Validated を付ける |
| 複数項目の関係(開始日≦終了日)を検証 | クラスに付けるカスタムアノテーション |
| エラーをJSONで返す | @RestControllerAdvice で2種類の例外を処理 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Spring Bootバリデーション一覧|@Validと@Validated」
依存関係を追加する
Spring Boot 2.3以降、Webのstarterにはバリデーションが含まれていません。spring-boot-starter-validation を明示的に追加します(実装はHibernate Validator)。
<!-- pom.xml(Spring Boot 4系) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
// build.gradle(Spring Boot 4系)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
Spring Boot 3系のWebのstarterは spring-boot-starter-web です。
| Spring Boot | Jakarta Validation | Hibernate Validator | パッケージ |
|---|---|---|---|
| 2.x | Bean Validation 2.0 | 6.x | javax.validation |
| 3.x | 3.0 | 8.0 | jakarta.validation |
| 4.x | 3.1 | 9.0 | jakarta.validation |
文字列のチェック:@NotNull・@NotEmpty・@NotBlankの違い
文字列の必須チェックは、基本的に @NotBlank を使います。3つの違いは、空文字と空白だけの文字列をどう扱うかです。
| 入力値 | @NotNull |
@NotEmpty |
@NotBlank |
|---|---|---|---|
null |
エラー | エラー | エラー |
""(空文字) |
OK | エラー | エラー |
" "(空白だけ) |
OK | OK | エラー |
"abc" |
OK | OK | OK |
@NotEmpty は文字列だけでなく、リストやMapが空でないことのチェックにも使えます。@NotBlank は文字列専用です。
@Sizeは「Stringのlength()」で数える
@Size の文字数は String#length()、つまりUTF-16の単位で数えます。絵文字などは1文字が2以上と数えられます。Java 21で同じ数え方を確認しました。
各文字列の length() と codePointCount() を表示した実行結果(OpenJDK 21.0.10):
abc length()=3 codePointCount=3
あいう length()=3 codePointCount=3
😀 length()=2 codePointCount=1
👨👩👧 length()=8 codePointCount=5
ひらがな・漢字は1文字=1、絵文字は2以上になります。「見た目の文字数」で制限したい場合は、後述のカスタムアノテーションで数え方を決めます。DBの列の長さ(文字数かバイト数か)とも合わせて上限を決めると安心です。
@Patternは「文字列全体」が一致するかで判定する
@Pattern は部分一致ではなく、文字列全体が正規表現に一致するかを見ます(Matcher#matches() と同じ考え方)。そのため ^ と $ は付けなくても全体一致になります。
正規表現 [0-9]{3}-[0-9]{4} で matches() と find() を比べた実行結果(OpenJDK 21.0.10):
123-4567 matches()=true find()=true
郵便123-4567 matches()=false find()=true
123-45678 matches()=false find()=true
@Emailは形式の大まかなチェック
@Email は「@の前後に適切な文字がある」程度のチェックで、ドメインにドットがない形(user@localhost など)も通ります。「ドット付きのドメインに限りたい」など、より厳しくしたいときは regexp 属性を足します。
@NotBlank
@Email(regexp = "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")
String email
数値と日付のチェック
整数は @Min/@Max、小数は @DecimalMin/@DecimalMax、符号だけなら @Positive などを使います。
| やりたいこと | 書き方 | ポイント |
|---|---|---|
| 1〜99の整数 | @Min(1) @Max(99) int quantity |
値は long で指定 |
| 0より大きい小数 | @DecimalMin(value = "0.0", inclusive = false) BigDecimal price |
値は文字列。inclusive = false で「より大きい」 |
| 0以上 | @PositiveOrZero int stock |
0を含む |
| 整数6桁・小数2桁まで | @Digits(integer = 6, fraction = 2) BigDecimal amount |
桁数の上限 |
| 今日より前 | @Past LocalDate birthDate |
今日は含まない |
| 今日以降 | @FutureOrPresent LocalDate startDate |
今日を含む |
@Min と @Max の標準の対象は整数系と BigDecimal・BigInteger です(仕様上、double と float は丸め誤差のため対象外。Hibernate Validatorは独自に対応しています)。小数の範囲は BigDecimal と @DecimalMin の組み合わせが確実です。
プリミティブ型とラッパー型
JSONで項目が送られてこなかったとき、int は0、Integer はnullになります。
| 型 | 項目なしのとき | 必須チェックの書き方 |
|---|---|---|
int quantity |
0 | @Min(1) で0を弾く(@NotNull は意味がない) |
Integer quantity |
null | @NotNull @Min(1) |
「未入力」と「0」を区別したい項目はラッパー型にして @NotNull を付けます。
@Validと@Validatedの違い
@Valid はJakarta標準、@Validated はSpring独自のアノテーションです。どちらもControllerの引数で検証を起動できますが、使える場所と機能が違います。
| 項目 | @Valid |
@Validated |
|---|---|---|
| パッケージ | jakarta.validation.Valid |
org.springframework.validation.annotation.Validated |
Controllerの @RequestBody 引数 |
使える | 使える |
| グループ指定 | できない(Defaultグループのみ) | できる(@Validated(OnCreate.class)) |
| フィールドに付けてネスト検証 | できる | できない(フィールドには付けられない) |
| クラスに付ける | 意味を持たない | Spring Beanのメソッド検証を有効にする |
| 主な使いどころ | ネスト・リストの中身・通常の入力チェック | グループ切り替え・Serviceのメソッド検証 |
迷ったら、Controllerの引数とネストには @Valid、グループを使うときとServiceクラスには @Validated と覚えておくと整理しやすくなります。
ネスト:子オブジェクトの中も検証する
フィールドに @Valid を付けないと、子オブジェクトの中の制約は検証されません。リストの要素には、型引数に直接アノテーションを付けられます。
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import java.util.List;
public record OrderRequest(
@NotBlank @Size(max = 100) String customerName,
@Valid @NotNull
Address address, // Address の中の制約も検証
@NotEmpty
List<@Valid OrderLine> lines, // 各明細の中も検証
@Size(max = 5)
List<@NotBlank @Size(max = 20) String> tags // 各タグを検証
) {
public record Address(
@NotBlank @Pattern(regexp = "[0-9]{3}-[0-9]{4}") String postalCode,
@NotBlank String city) {}
public record OrderLine(
@NotBlank String itemCode,
@Min(1) @Max(99) int quantity) {}
}
エラーの項目名は address.postalCode、lines[0].quantity、tags[1] のように、どの階層のどの要素かがわかる形になります。
グループ:作成時と更新時でルールを変える
グループを使うと、同じクラスでも場面ごとに検証するアノテーションを切り替えられます。グループはただのインターフェースです。
import jakarta.validation.groups.Default;
public interface OnCreate extends Default {}
public interface OnUpdate extends Default {}
import jakarta.validation.constraints.*;
public record MemberForm(
@Null(groups = OnCreate.class) // 作成時はidを送らせない
@NotNull(groups = OnUpdate.class) // 更新時はidが必須
Long id,
@NotBlank @Size(max = 50) // groups 指定なし = Default グループ
String name
) {}
@PostMapping
public MemberResponse create(@Validated(OnCreate.class) @RequestBody MemberForm form) { ... }
@PutMapping("/{id}")
public MemberResponse update(@Validated(OnUpdate.class) @RequestBody MemberForm form) { ... }
| グループ定義 | @Validated(OnCreate.class) で検証される制約 |
|---|---|
interface OnCreate {} |
groups = OnCreate.class の制約だけ(name の @NotBlank は検証されない) |
interface OnCreate extends Default {} |
OnCreateの制約 + groups指定なしの制約 |
groupsを指定しない制約は Default グループに属します。グループのインターフェースで Default を継承しておくと、共通のルールを書き直さずに済みます。
メソッド検証:Serviceの引数をチェックする
Spring Beanのクラスに @Validated を付けると、メソッドの引数や戻り値に付けた制約が検証されます。Spring Bootは、バリデーションの実装がクラスパスにあれば、この仕組み(MethodValidationPostProcessor)を自動で有効にします。
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@Service
@Validated
public class StockService {
public int reserve(@NotBlank String itemCode, @Positive int quantity) {
// ここに来た時点で itemCode と quantity はチェック済み
return quantity;
}
}
違反があると jakarta.validation.ConstraintViolationException が発生します。何もしないと500エラーになるので、入力の問題として扱うなら後述のハンドラーで400に変換します。
Controllerでの使い方:3つのパターン
Spring Framework 6.1(Spring Boot 3.2)以降、Controllerは引数に直接付けた制約を自動で検証します。パス変数やクエリパラメータのチェックのために、Controllerクラスへ @Validated を付ける必要はありません。
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/members")
public class MemberController {
// パターン1:リクエストボディ(JSON)
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public MemberResponse create(@Valid @RequestBody MemberCreateRequest request) {
return new MemberResponse(1L, request.name());
}
// パターン2:パス変数・クエリパラメータに直接付ける
@GetMapping("/{id}")
public MemberResponse get(@PathVariable @Positive long id) {
return new MemberResponse(id, "sample");
}
@GetMapping
public List<MemberResponse> search(
@RequestParam @Size(max = 50) String keyword,
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int limit) {
return List.of();
}
// パターン3:リストを丸ごと受け取る
@PostMapping("/bulk")
public int bulk(@RequestBody @Size(max = 100) List<@Valid MemberCreateRequest> requests) {
return requests.size();
}
}
public record MemberCreateRequest(
@NotBlank @Size(max = 50) String name,
@NotBlank @Email String email) {}
public record MemberResponse(long id, String name) {}
どの書き方をしたかで、発生する例外が変わります。公式リファレンスでは、次のように説明されています。
| 引数の書き方 | 発生する例外 | ステータス |
|---|---|---|
@Valid @RequestBody Form form(1つのオブジェクト) |
MethodArgumentNotValidException |
400 |
@PathVariable @Positive long id など、引数に制約を直接付ける |
HandlerMethodValidationException |
400 |
@RequestBody List<@Valid Form> などのリスト・Map |
HandlerMethodValidationException |
400 |
@Valid の引数に @NotNull などの制約も付ける |
HandlerMethodValidationException |
400 |
| 戻り値に制約を付けて違反した | HandlerMethodValidationException |
500 |
メソッドのどこかに「引数に直接付けた制約」があると、そのメソッドは丸ごとメソッド検証の対象になり、HandlerMethodValidationException にまとめられます。そのため、エラーハンドラーは2つの例外の両方を用意するのが基本です。
Controllerクラスに
@Validatedが付いていると、Spring MVC組み込みの検証ではなく、Serviceと同じAOPの仕組みが動き、ConstraintViolationExceptionになります。公式リファレンスは、組み込みの検証を使うためにクラスの@Validatedを外すよう案内しています。
エラーをJSONで返す:@RestControllerAdvice
3種類の例外を1か所で受け止め、同じ形のJSONに揃えます。ここでは {status, message, errors: [{field, message}]} という独自の形にしています。RFC 9457形式(ProblemDetail)で返す方法は、別記事「Spring BootでREST APIを作る」で解説しています。
import jakarta.validation.ConstraintViolationException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.validation.method.ParameterValidationResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
@RestControllerAdvice
public class ValidationErrorHandler {
public record ErrorItem(String field, String message) {}
public record ErrorBody(int status, String message, List<ErrorItem> errors) {}
// 1. @Valid @RequestBody で1つのオブジェクトを検証したとき
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorBody> handleBody(MethodArgumentNotValidException ex) {
List<ErrorItem> items = new ArrayList<>();
for (FieldError fe : ex.getBindingResult().getFieldErrors()) {
items.add(new ErrorItem(fe.getField(), fe.getDefaultMessage()));
}
// クラスに付けた制約(相関チェックなど)のエラー
ex.getBindingResult().getGlobalErrors().forEach(ge ->
items.add(new ErrorItem(ge.getObjectName(), ge.getDefaultMessage())));
return ResponseEntity.badRequest()
.body(new ErrorBody(400, "入力内容に誤りがあります", items));
}
// 2. 引数に直接付けた制約・リストの検証(Spring Framework 6.1 以降)
@ExceptionHandler(HandlerMethodValidationException.class)
public ResponseEntity<ErrorBody> handleMethod(HandlerMethodValidationException ex) {
List<ErrorItem> items = new ArrayList<>();
for (ParameterValidationResult result : ex.getParameterValidationResults()) {
String param = result.getMethodParameter().getParameterName();
for (MessageSourceResolvable error : result.getResolvableErrors()) {
String field = (error instanceof FieldError fe) ? param + "." + fe.getField() : param;
items.add(new ErrorItem(field, error.getDefaultMessage()));
}
}
int status = ex.getStatusCode().value(); // 引数なら400、戻り値なら500
return ResponseEntity.status(status)
.body(new ErrorBody(status, "入力内容に誤りがあります", items));
}
// 3. @Validated を付けた Service などのメソッド検証
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorBody> handleViolation(ConstraintViolationException ex) {
List<ErrorItem> items = ex.getConstraintViolations().stream()
.map(v -> new ErrorItem(v.getPropertyPath().toString(), v.getMessage()))
.toList();
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorBody(400, "入力内容に誤りがあります", items));
}
}
| 例外 | エラーの取り出し方 | 項目名の取り方 |
|---|---|---|
MethodArgumentNotValidException |
getBindingResult().getFieldErrors() |
FieldError#getField() |
HandlerMethodValidationException |
getParameterValidationResults() → getResolvableErrors() |
getMethodParameter().getParameterName() |
ConstraintViolationException |
getConstraintViolations() |
getPropertyPath()(例:reserve.quantity) |
レスポンスの例
次は、上のハンドラーの仕様に基づく出力例です(この記事の環境ではSpringを起動していないため、実行結果ではありません)。
POST /api/members に {"name": "", "email": "abc"} を送った場合:
{
"status": 400,
"message": "入力内容に誤りがあります",
"errors": [
{ "field": "name", "message": "入力してください" },
{ "field": "email", "message": "メールアドレスの形式で入力してください" }
]
}
メッセージの文言は、次の章の ValidationMessages.properties の設定を前提にしています。エラーの並び順は保証されないので、テストで順番に依存しないようにします。
メッセージを日本語にする
メッセージの決め方は3通りあり、優先度は「アノテーションの message 属性 → 自分で置いたメッセージファイル → Hibernate Validator同梱の既定メッセージ」の順です。
| 方法 | 書き方 | 向いている場面 |
|---|---|---|
| アノテーションに直接書く | @NotBlank(message = "名前を入力してください") |
項目ごとに文言を変えたい |
ValidationMessages.properties |
既定メッセージのキーを上書き | アプリ全体で文言を統一したい |
messages.properties(SpringのMessageSource) |
message = "{member.name.required}" から参照 |
画面の文言と一緒に管理したい |
ValidationMessages.propertiesで全体の文言を揃える
src/main/resources/ValidationMessages.properties を置くと、標準アノテーションの既定メッセージを上書きできます。キーは jakarta.validation.constraints.{アノテーション名}.message です。
# src/main/resources/ValidationMessages.properties
jakarta.validation.constraints.NotBlank.message=入力してください
jakarta.validation.constraints.NotNull.message=入力してください
jakarta.validation.constraints.Size.message={min}〜{max}文字で入力してください
jakarta.validation.constraints.Max.message={value}以下で入力してください
jakarta.validation.constraints.Min.message={value}以上で入力してください
jakarta.validation.constraints.Email.message=メールアドレスの形式で入力してください
jakarta.validation.constraints.Pattern.message=形式が正しくありません
order.item.size=商品名は{max}文字以内で入力してください
{min} や {max}、{value} には、アノテーションの属性値が入ります。独自のキー(order.item.size)は @Size(max = 100, message = "{order.item.size}") のように参照します。
Java 9以降、.properties のリソースバンドルはUTF-8で読み込まれるため、日本語をそのまま書けます(\u 形式に変換する必要はありません)。Java 21で、UTF-8で保存したファイルが読めることを確認しました。
ResourceBundle.getBundle("ValidationMessages", Locale.JAPAN) で読み込んだ実行結果(OpenJDK 21.0.10):
入力してください
{min}〜{max}文字で入力してください
商品名は{max}文字以内で入力してください
ここで {min} などがそのまま残っているのは、ResourceBundleを直接読んだためです。Hibernate Validatorを通すと、この部分に属性値が埋め込まれます(埋め込み後の結果はこの環境では未確認です。仕様は公式ドキュメントで確認してください)。
Spring Bootでは、messages.properties(Springの MessageSource)のキーも制約のメッセージから参照できます。公式リファレンスによると、メッセージ中の {パラメータ} はまずアプリの MessageSource で解決され、そのあとBean Validationの標準の仕組みで埋め込みが行われます。画面の文言と同じファイルで管理したい場合に便利です。
カスタムアノテーションを作る
既存のアノテーションを組み合わせる(合成アノテーション)
よく使う組み合わせは、1つのアノテーションにまとめられます。次は「英大文字3桁-数字4桁」の備品コードを表す @StockCode です。
package com.example.validation;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jakarta.validation.ReportAsSingleViolation;
import jakarta.validation.constraints.Pattern;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Pattern(regexp = "[A-Z]{3}-[0-9]{4}")
@ReportAsSingleViolation // 違反時は @StockCode のメッセージ1件だけにする
@Constraint(validatedBy = {}) // 独自のロジックなし(中のアノテーションで検証)
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE})
@Retention(RUNTIME)
@Documented
public @interface StockCode {
String message() default "{stock.code.invalid}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
message・groups・payload の3つの属性は、制約アノテーションに必須です。どれかが欠けると、検証の実行時にエラーになります。
相関チェック:開始日と終了日の関係を見る
複数の項目の関係を検証するときは、クラス(record)に付けるアノテーションを作ります。ここでは、開始日と終了日を持つクラスに共通のインターフェースを用意し、どのリクエストでも使い回せるようにしています。
package com.example.validation;
import java.time.LocalDate;
public interface HasPeriod {
LocalDate startDate();
LocalDate endDate();
}
package com.example.validation;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Constraint(validatedBy = ValidPeriodValidator.class)
@Target(TYPE)
@Retention(RUNTIME)
@Documented
public @interface ValidPeriod {
String message() default "終了日は開始日以降の日付を指定してください";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
package com.example.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class ValidPeriodValidator implements ConstraintValidator<ValidPeriod, HasPeriod> {
@Override
public boolean isValid(HasPeriod value, ConstraintValidatorContext context) {
if (value == null || value.startDate() == null || value.endDate() == null) {
return true; // null のチェックは @NotNull に任せる
}
if (!value.endDate().isBefore(value.startDate())) {
return true;
}
// エラーを「endDate」項目のエラーとして登録し直す
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
.addPropertyNode("endDate")
.addConstraintViolation();
return false;
}
}
package com.example.validation;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
@ValidPeriod
public record ReservationRequest(
@StockCode String equipmentCode,
@NotNull LocalDate startDate,
@NotNull LocalDate endDate
) implements HasPeriod {}
ポイントは3つです。
| ポイント | 理由 |
|---|---|
nullのときは true を返す |
必須チェックは @NotNull の役割。二重にエラーが出るのを防ぐ |
addPropertyNode("endDate") |
クラス全体のエラーではなく endDate 項目のエラーになり、画面やJSONで項目の横に表示できる |
| インターフェースで受ける | recordのアクセサーがそのままインターフェースの実装になり、別のリクエストでも使い回せる |
検証内容の確認:この環境ではHibernate Validatorを入手できないため、jakarta.validation の必要なインターフェースだけを持つスタブを作り、上の4ファイルをJava 21でコンパイルしたうえで、isValid を直接呼び出して動きを確かめました(記録用のダミーのコンテキストを渡しています)。
ok -> true
same -> true
ng -> false [disableDefault, template=終了日は開始日以降の日付を指定してください, node=endDate, added]
@StockCode の @Pattern: [A-Z]{3}-[0-9]{4}
同じ日(same)は通り、終了日が前の日(ng)のときだけ endDate 項目のエラーとして登録されます。Hibernate Validator上でのエラーの出方は公式ドキュメントの仕様に沿って書いています。
もっと手軽な方法:@AssertTrueのメソッド
1つのクラスでしか使わない相関チェックなら、booleanを返す is〜 メソッドに @AssertTrue を付ける方法もあります。
public record PasswordChangeRequest(
@NotBlank @Size(min = 12) String newPassword,
@NotBlank String confirmPassword) {
@AssertTrue(message = "確認用パスワードが一致しません")
public boolean isPasswordConfirmed() {
return newPassword == null || newPassword.equals(confirmPassword);
}
}
エラーの項目名は passwordConfirmed になります。使い回すならカスタムアノテーション、その場限りなら @AssertTrue と使い分けます。
javaxからjakartaへ移行するときの注意
Spring Boot 3以降は jakarta.validation パッケージを使います。javax.validation のアノテーションが残っていると、Hibernate Validator 8以降はそれを制約として認識しないため、エラーにならずに検証だけが行われない状態になります。移行後は、エラーになるはずの入力を送るテストで確かめておくと安心です。
| 確認すること | 内容 |
|---|---|
| import文 | javax.validation.* → jakarta.validation.* |
| メッセージのキー | javax.validation.constraints.NotBlank.message → jakarta.validation.constraints.NotBlank.message |
| 依存関係 | javax.validation:validation-api が残っていないか |
| 置き換えてはいけないもの | javax.sql・javax.crypto など、JDK標準のパッケージ |
置き換えは、javax.validation だけを対象にするのが安全です。次のコマンドで、サンプルのファイルを変換しました。
grep -rn --include='*.java' 'import javax\.' src
grep -rl --include='*.java' 'javax\.validation' src | xargs sed -i 's/javax\.validation/jakarta.validation/g'
変換後のファイル(GNU sed 4.9で実行):
package com.example;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import javax.crypto.Cipher;
import javax.sql.DataSource;
javax.crypto と javax.sql はJDKの一部なので、そのまま残ります。ValidationMessages.properties のキーも忘れずに置き換えます。大きなコードベースでは、OpenRewriteのJakarta移行レシピのような自動変換ツールも選択肢になります。
検証が動かないときのチェックリスト
| 症状 | 確認すること | 直し方 |
|---|---|---|
| 何を送ってもエラーにならない | spring-boot-starter-validation があるか |
依存関係を追加 |
| 何を送ってもエラーにならない | importが javax.validation になっていないか |
jakarta.validation に置き換え |
| 子オブジェクトの中が検証されない | フィールドに @Valid があるか |
@Valid Address address |
| リストの要素が検証されない | 型引数に付けているか | List<@Valid Item> |
@NotNull が効かない |
型が int などのプリミティブでないか |
Integer に変える |
想定外の ConstraintViolationException で500 |
Controllerクラスに @Validated がないか |
クラスの @Validated を外す、またはハンドラーを追加 |
| グループ指定でほかの制約が検証されない | グループが Default を継承しているか |
interface OnCreate extends Default {} |
| Serviceのメソッド検証が効かない | クラスに @Validated があるか、同じクラス内から呼んでいないか |
別のBeanから呼ぶ(AOPの仕組みのため) |
よくある質問
Q. @Validと@Validated、Controllerではどちらを使えばよいですか?
A. グループを使わないなら @Valid で十分です。グループで作成時・更新時のルールを切り替えるときだけ @Validated(OnCreate.class) を使います。ネストした子オブジェクトには、フィールドに @Valid を付けます(@Validated はフィールドに付けられません)。
Q. @PathVariableや@RequestParamのチェックに、Controllerクラスの@Validatedは必要ですか?
A. Spring Framework 6.1(Spring Boot 3.2)以降は不要です。引数に @Positive などを直接付けるだけで、組み込みのメソッド検証が動き、HandlerMethodValidationException になります。クラスに @Validated を付けると別の仕組み(ConstraintViolationException)になるため、公式リファレンスでは外すよう案内されています。
Q. MethodArgumentNotValidExceptionだけ処理すれば足りますか?
A. HandlerMethodValidationException も処理しておくのが安心です。引数に直接付けた制約や、List を受け取る引数の検証では、こちらの例外になります。両方を同じ形のJSONに揃えると、APIを使う側が扱いやすくなります。
まとめ
spring-boot-starter-validationを追加し、制約はjakarta.validation.constraintsのアノテーションで付ける- nullをエラーにするのは
@NotNull・@NotEmpty・@NotBlankだけ。形式チェックと必須チェックは組み合わせる @Validはネストとリストの中身、@ValidatedはグループとServiceのメソッド検証に使う- Controllerは引数に直接制約を付けられる(Spring Framework 6.1以降)。例外は
MethodArgumentNotValidExceptionとHandlerMethodValidationExceptionの2つを処理する - 日本語メッセージは
ValidationMessages.properties(UTF-8のまま書ける)で一括設定する - 相関チェックはクラスに付けるカスタムアノテーションで、
addPropertyNodeで項目にエラーをひも付ける - javaxからの移行は
javax.validationだけを置き換え、エラーになる入力を送って検証が動くことを確かめる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Spring Bootバリデーション一覧|@Validと@Validated」
株式会社RJCでは、Spring Bootの入力チェックの設計を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。