REST APIにおいて、クライアントからのリクエストデータを適切に検証することは、アプリケーションの堅牢性とセキュリティを確保する上で不可欠です。Spring BootはJakarta Bean Validation(旧Java Bean Validation)を統合しており、アノテーションベースで宣言的にバリデーションルールを定義できます。本記事では、標準バリデーションアノテーションの使い方からカスタムバリデーションの作成、グループバリデーションによる条件分岐まで、実践的なバリデーション実装を解説します。
実行環境と前提条件#
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 |
バージョン・要件 |
| Java |
17以上 |
| Spring Boot |
3.4.x |
| ビルドツール |
Maven または Gradle |
| IDE |
VS Code(Extension Pack for Javaインストール済み) |
また、本記事はSpring Boot REST API入門 - @RestControllerでCRUDエンドポイントを実装するの続編として、基本的なSpring Boot REST APIの知識があることを前提としています。
期待される学習成果#
本記事を読み終えると、以下のことができるようになります。
- 標準バリデーションアノテーションを使って入力検証を実装できる
- カスタムバリデーションアノテーションを作成して独自の検証ルールを定義できる
- グループバリデーションを使って操作ごとに異なる検証ルールを適用できる
- バリデーションエラーを適切にハンドリングしてクライアントに返却できる
Bean Validationの依存関係#
Spring Boot 3.x では、spring-boot-starter-validationを追加することでBean Validationが利用可能になります。
Mavenの場合#
1
2
3
4
|
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
|
Gradleの場合#
1
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
この依存関係により、Jakarta Bean Validation API(jakarta.validationパッケージ)と参照実装であるHibernate Validatorが自動的に追加されます。
標準バリデーションアノテーション一覧#
Jakarta Bean Validation 3.0では、以下の標準アノテーションが提供されています。これらはすべてjakarta.validation.constraintsパッケージに含まれています。
Null/非Null検証#
| アノテーション |
説明 |
対象型 |
@Null |
nullであることを検証 |
任意の型 |
@NotNull |
nullでないことを検証 |
任意の型 |
@NotEmpty |
nullでなく、空でないことを検証 |
CharSequence, Collection, Map, 配列 |
@NotBlank |
nullでなく、空白文字以外を含むことを検証 |
CharSequence |
数値検証#
| アノテーション |
説明 |
対象型 |
@Min(value) |
指定値以上であることを検証 |
整数型(BigDecimal, BigInteger, byte, short, int, long) |
@Max(value) |
指定値以下であることを検証 |
整数型 |
@DecimalMin(value) |
指定値以上であることを検証(文字列で指定) |
数値型、CharSequence |
@DecimalMax(value) |
指定値以下であることを検証(文字列で指定) |
数値型、CharSequence |
@Positive |
正の数であることを検証(0は無効) |
数値型 |
@PositiveOrZero |
0以上であることを検証 |
数値型 |
@Negative |
負の数であることを検証(0は無効) |
数値型 |
@NegativeOrZero |
0以下であることを検証 |
数値型 |
@Digits(integer, fraction) |
整数部と小数部の桁数を検証 |
数値型、CharSequence |
サイズ・長さ検証#
| アノテーション |
説明 |
対象型 |
@Size(min, max) |
サイズが指定範囲内であることを検証 |
CharSequence, Collection, Map, 配列 |
日時検証#
| アノテーション |
説明 |
対象型 |
@Past |
過去の日時であることを検証 |
Date, Calendar, java.time.* |
@PastOrPresent |
過去または現在の日時であることを検証 |
Date, Calendar, java.time.* |
@Future |
未来の日時であることを検証 |
Date, Calendar, java.time.* |
@FutureOrPresent |
現在または未来の日時であることを検証 |
Date, Calendar, java.time.* |
パターン検証#
| アノテーション |
説明 |
対象型 |
@Pattern(regexp) |
正規表現にマッチすることを検証 |
CharSequence |
@Email |
メールアドレス形式であることを検証 |
CharSequence |
論理値検証#
| アノテーション |
説明 |
対象型 |
@AssertTrue |
trueであることを検証 |
boolean, Boolean |
@AssertFalse |
falseであることを検証 |
boolean, Boolean |
DTOへのバリデーションアノテーション適用#
リクエストボディを受け取るDTOクラスにバリデーションアノテーションを適用します。
ユーザー登録用DTOの例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
package com.example.demoapi.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;
public class UserCreateRequest {
@NotBlank(message = "ユーザー名は必須です")
@Size(min = 3, max = 50, message = "ユーザー名は3文字以上50文字以下で入力してください")
private String username;
@NotBlank(message = "メールアドレスは必須です")
@Email(message = "有効なメールアドレスを入力してください")
private String email;
@NotBlank(message = "パスワードは必須です")
@Size(min = 8, max = 100, message = "パスワードは8文字以上100文字以下で入力してください")
@Pattern(
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$",
message = "パスワードは大文字、小文字、数字をそれぞれ1文字以上含める必要があります"
)
private String password;
@NotNull(message = "年齢は必須です")
@Min(value = 0, message = "年齢は0以上で入力してください")
private Integer age;
@Past(message = "生年月日は過去の日付を入力してください")
private LocalDate birthDate;
@Pattern(regexp = "^\\d{3}-\\d{4}-\\d{4}$", message = "電話番号はXXX-XXXX-XXXX形式で入力してください")
private String phoneNumber;
// コンストラクタ、ゲッター、セッター
public UserCreateRequest() {
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public LocalDate getBirthDate() {
return birthDate;
}
public void setBirthDate(LocalDate birthDate) {
this.birthDate = birthDate;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
}
|
コントローラーでのバリデーション適用#
DTOにバリデーションアノテーションを定義しただけでは検証は実行されません。コントローラーのメソッドパラメータに@Validまたは@Validatedを付与する必要があります。
@Validと@Validatedの違い#
| 項目 |
@Valid |
@Validated |
| パッケージ |
jakarta.validation |
org.springframework.validation.annotation |
| グループ指定 |
不可 |
可能 |
| ネストしたオブジェクトの検証 |
可能 |
可能 |
| 用途 |
標準的なバリデーション |
グループバリデーション使用時 |
コントローラーの実装例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
package com.example.demoapi.controller;
import com.example.demoapi.dto.UserCreateRequest;
import com.example.demoapi.dto.UserResponse;
import com.example.demoapi.service.UserService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<UserResponse> createUser(
@Valid @RequestBody UserCreateRequest request) {
UserResponse response = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
|
@Validを付与することで、リクエストボディのデシリアライズ後にバリデーションが実行されます。バリデーションエラーが発生した場合、MethodArgumentNotValidExceptionがスローされます。
バリデーションエラーのハンドリング#
バリデーションエラーが発生した際、デフォルトでは400 Bad Requestが返されますが、エラー詳細がクライアントにとってわかりやすい形式ではありません。@RestControllerAdviceを使ってカスタムエラーレスポンスを返却します。
エラーレスポンスDTOの定義#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
package com.example.demoapi.dto;
import java.time.LocalDateTime;
import java.util.List;
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
private List<FieldError> fieldErrors;
public ErrorResponse() {
this.timestamp = LocalDateTime.now();
}
public static class FieldError {
private String field;
private String message;
private Object rejectedValue;
public FieldError(String field, String message, Object rejectedValue) {
this.field = field;
this.message = message;
this.rejectedValue = rejectedValue;
}
// ゲッター
public String getField() {
return field;
}
public String getMessage() {
return message;
}
public Object getRejectedValue() {
return rejectedValue;
}
}
// ゲッター、セッター
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public List<FieldError> getFieldErrors() {
return fieldErrors;
}
public void setFieldErrors(List<FieldError> fieldErrors) {
this.fieldErrors = fieldErrors;
}
}
|
グローバル例外ハンドラーの実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
package com.example.demoapi.exception;
import com.example.demoapi.dto.ErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex,
HttpServletRequest request) {
List<ErrorResponse.FieldError> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new ErrorResponse.FieldError(
error.getField(),
error.getDefaultMessage(),
error.getRejectedValue()))
.toList();
ErrorResponse response = new ErrorResponse();
response.setStatus(HttpStatus.BAD_REQUEST.value());
response.setError("Validation Failed");
response.setMessage("入力値に誤りがあります");
response.setPath(request.getRequestURI());
response.setFieldErrors(fieldErrors);
return ResponseEntity.badRequest().body(response);
}
}
|
エラーレスポンスの例#
バリデーションエラーが発生した場合、以下のようなJSON形式でレスポンスが返却されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
{
"timestamp": "2026-01-04T17:30:00",
"status": 400,
"error": "Validation Failed",
"message": "入力値に誤りがあります",
"path": "/api/users",
"fieldErrors": [
{
"field": "username",
"message": "ユーザー名は3文字以上50文字以下で入力してください",
"rejectedValue": "ab"
},
{
"field": "email",
"message": "有効なメールアドレスを入力してください",
"rejectedValue": "invalid-email"
}
]
}
|
ネストしたオブジェクトのバリデーション#
DTOがネストしたオブジェクトを持つ場合、@Validアノテーションを使って再帰的にバリデーションを適用できます。
住所情報を含むDTOの例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
package com.example.demoapi.dto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
public class UserWithAddressRequest {
@NotBlank(message = "ユーザー名は必須です")
private String username;
@NotNull(message = "住所は必須です")
@Valid // ネストしたオブジェクトのバリデーションを有効化
private AddressRequest address;
// ゲッター、セッター
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public AddressRequest getAddress() {
return address;
}
public void setAddress(AddressRequest address) {
this.address = address;
}
public static class AddressRequest {
@NotBlank(message = "郵便番号は必須です")
@Pattern(regexp = "^\\d{3}-\\d{4}$", message = "郵便番号はXXX-XXXX形式で入力してください")
private String postalCode;
@NotBlank(message = "都道府県は必須です")
private String prefecture;
@NotBlank(message = "市区町村は必須です")
@Size(max = 100, message = "市区町村は100文字以下で入力してください")
private String city;
@Size(max = 200, message = "番地は200文字以下で入力してください")
private String street;
// ゲッター、セッター
public String getPostalCode() {
return postalCode;
}
public void setPostalCode(String postalCode) {
this.postalCode = postalCode;
}
public String getPrefecture() {
return prefecture;
}
public void setPrefecture(String prefecture) {
this.prefecture = prefecture;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
}
}
|
@Validを付与したフィールドは、そのオブジェクト内のバリデーションアノテーションも検証対象となります。
コレクション要素のバリデーション#
リストやマップの各要素に対してバリデーションを適用することも可能です。
リスト要素のバリデーション#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
package com.example.demoapi.dto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import java.util.List;
public class OrderRequest {
@NotBlank(message = "顧客コードは必須です")
private String customerCode;
@NotEmpty(message = "注文明細は1件以上必要です")
@Size(max = 100, message = "注文明細は100件以下にしてください")
private List<@Valid OrderItemRequest> items;
// ゲッター、セッター
public String getCustomerCode() {
return customerCode;
}
public void setCustomerCode(String customerCode) {
this.customerCode = customerCode;
}
public List<OrderItemRequest> getItems() {
return items;
}
public void setItems(List<OrderItemRequest> items) {
this.items = items;
}
public static class OrderItemRequest {
@NotBlank(message = "商品コードは必須です")
private String productCode;
@jakarta.validation.constraints.Min(value = 1, message = "数量は1以上で入力してください")
private int quantity;
// ゲッター、セッター
public String getProductCode() {
return productCode;
}
public void setProductCode(String productCode) {
this.productCode = productCode;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
}
}
|
カスタムバリデーションアノテーションの作成#
標準アノテーションでは対応できない検証ロジックが必要な場合、カスタムバリデーションアノテーションを作成します。
カスタムアノテーションの構成要素#
カスタムバリデーションは以下の2つの要素で構成されます。
- アノテーション定義: 制約を宣言するアノテーション
- バリデータ実装: 検証ロジックを実装する
ConstraintValidator
電話番号検証アノテーションの例#
アノテーション定義#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
package com.example.demoapi.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface PhoneNumber {
String message() default "有効な電話番号を入力してください";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// カスタム属性: 電話番号の種類を指定
PhoneType type() default PhoneType.ANY;
enum PhoneType {
MOBILE, // 携帯電話
LANDLINE, // 固定電話
ANY // どちらでも可
}
}
|
バリデータ実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
package com.example.demoapi.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
private static final Pattern MOBILE_PATTERN =
Pattern.compile("^0[789]0-\\d{4}-\\d{4}$");
private static final Pattern LANDLINE_PATTERN =
Pattern.compile("^0\\d{1,4}-\\d{1,4}-\\d{4}$");
private PhoneNumber.PhoneType phoneType;
@Override
public void initialize(PhoneNumber constraintAnnotation) {
this.phoneType = constraintAnnotation.type();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// nullは他の制約(@NotNullなど)で検証するため、ここではtrueを返す
if (value == null || value.isBlank()) {
return true;
}
return switch (phoneType) {
case MOBILE -> MOBILE_PATTERN.matcher(value).matches();
case LANDLINE -> LANDLINE_PATTERN.matcher(value).matches();
case ANY -> MOBILE_PATTERN.matcher(value).matches()
|| LANDLINE_PATTERN.matcher(value).matches();
};
}
}
|
カスタムアノテーションの使用#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
package com.example.demoapi.dto;
import com.example.demoapi.validation.PhoneNumber;
import com.example.demoapi.validation.PhoneNumber.PhoneType;
import jakarta.validation.constraints.NotBlank;
public class ContactRequest {
@NotBlank(message = "名前は必須です")
private String name;
@PhoneNumber(type = PhoneType.MOBILE, message = "携帯電話番号を入力してください")
private String mobilePhone;
@PhoneNumber(type = PhoneType.LANDLINE, message = "固定電話番号を入力してください")
private String landlinePhone;
// ゲッター、セッター
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMobilePhone() {
return mobilePhone;
}
public void setMobilePhone(String mobilePhone) {
this.mobilePhone = mobilePhone;
}
public String getLandlinePhone() {
return landlinePhone;
}
public void setLandlinePhone(String landlinePhone) {
this.landlinePhone = landlinePhone;
}
}
|
クラスレベルバリデーションの実装#
複数のフィールド間の整合性を検証する場合、クラスレベルのバリデーションを使用します。
パスワード確認用アノテーションの例#
アノテーション定義#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package com.example.demoapi.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Constraint(validatedBy = PasswordMatchValidator.class)
@Target(ElementType.TYPE) // クラスレベルに適用
@Retention(RetentionPolicy.RUNTIME)
public @interface PasswordMatch {
String message() default "パスワードが一致しません";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String passwordField() default "password";
String confirmPasswordField() default "confirmPassword";
}
|
バリデータ実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
package com.example.demoapi.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
public class PasswordMatchValidator implements ConstraintValidator<PasswordMatch, Object> {
private String passwordField;
private String confirmPasswordField;
@Override
public void initialize(PasswordMatch constraintAnnotation) {
this.passwordField = constraintAnnotation.passwordField();
this.confirmPasswordField = constraintAnnotation.confirmPasswordField();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
BeanWrapper beanWrapper = new BeanWrapperImpl(value);
Object password = beanWrapper.getPropertyValue(passwordField);
Object confirmPassword = beanWrapper.getPropertyValue(confirmPasswordField);
boolean isValid = (password == null && confirmPassword == null)
|| (password != null && password.equals(confirmPassword));
if (!isValid) {
// デフォルトのエラーメッセージを無効化
context.disableDefaultConstraintViolation();
// confirmPasswordフィールドにエラーを紐づけ
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
.addPropertyNode(confirmPasswordField)
.addConstraintViolation();
}
return isValid;
}
}
|
クラスレベルアノテーションの使用#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
package com.example.demoapi.dto;
import com.example.demoapi.validation.PasswordMatch;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@PasswordMatch(message = "パスワードと確認用パスワードが一致しません")
public class PasswordChangeRequest {
@NotBlank(message = "現在のパスワードは必須です")
private String currentPassword;
@NotBlank(message = "新しいパスワードは必須です")
@Size(min = 8, max = 100, message = "パスワードは8文字以上100文字以下で入力してください")
private String password;
@NotBlank(message = "確認用パスワードは必須です")
private String confirmPassword;
// ゲッター、セッター
public String getCurrentPassword() {
return currentPassword;
}
public void setCurrentPassword(String currentPassword) {
this.currentPassword = currentPassword;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getConfirmPassword() {
return confirmPassword;
}
public void setConfirmPassword(String confirmPassword) {
this.confirmPassword = confirmPassword;
}
}
|
グループバリデーションによる条件分岐#
同じDTOを複数の操作(登録、更新など)で使用する場合、操作ごとに異なるバリデーションルールを適用したいことがあります。グループバリデーションを使うことで、この要件を実現できます。
グループインターフェースの定義#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package com.example.demoapi.validation.group;
import jakarta.validation.groups.Default;
public interface ValidationGroups {
// 登録時のバリデーショングループ
interface Create extends Default {
}
// 更新時のバリデーショングループ
interface Update extends Default {
}
}
|
グループを指定したDTOの定義#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
package com.example.demoapi.dto;
import com.example.demoapi.validation.group.ValidationGroups.Create;
import com.example.demoapi.validation.group.ValidationGroups.Update;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Null;
import jakarta.validation.constraints.Size;
public class UserRequest {
// 登録時はnull必須、更新時は必須
@Null(groups = Create.class, message = "登録時にIDは指定できません")
@NotNull(groups = Update.class, message = "更新時はIDが必須です")
private Long id;
@NotBlank(message = "ユーザー名は必須です")
@Size(min = 3, max = 50, message = "ユーザー名は3文字以上50文字以下で入力してください")
private String username;
@NotBlank(message = "メールアドレスは必須です")
@Email(message = "有効なメールアドレスを入力してください")
private String email;
// 登録時は必須、更新時は任意(nullの場合は変更なし)
@NotBlank(groups = Create.class, message = "パスワードは必須です")
@Size(min = 8, max = 100, message = "パスワードは8文字以上100文字以下で入力してください")
private String password;
// ゲッター、セッター
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
|
コントローラーでのグループ指定#
グループバリデーションを使用する場合は、@Validではなく@Validatedを使用し、適用するグループを指定します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
package com.example.demoapi.controller;
import com.example.demoapi.dto.UserRequest;
import com.example.demoapi.dto.UserResponse;
import com.example.demoapi.service.UserService;
import com.example.demoapi.validation.group.ValidationGroups.Create;
import com.example.demoapi.validation.group.ValidationGroups.Update;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<UserResponse> createUser(
@Validated(Create.class) @RequestBody UserRequest request) {
UserResponse response = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(
@PathVariable Long id,
@Validated(Update.class) @RequestBody UserRequest request) {
request.setId(id);
UserResponse response = userService.updateUser(request);
return ResponseEntity.ok(response);
}
}
|
グループシーケンスによる順序制御#
バリデーションを段階的に実行し、前段階でエラーがあれば後続のバリデーションをスキップしたい場合、@GroupSequenceを使用します。
グループシーケンスの定義#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package com.example.demoapi.validation.group;
import jakarta.validation.GroupSequence;
import jakarta.validation.groups.Default;
public interface OrderedValidation {
interface First {
}
interface Second {
}
interface Third {
}
@GroupSequence({Default.class, First.class, Second.class, Third.class})
interface OrderedChecks {
}
}
|
シーケンスを適用したDTOの例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
package com.example.demoapi.dto;
import com.example.demoapi.validation.group.OrderedValidation.First;
import com.example.demoapi.validation.group.OrderedValidation.Second;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
public class RegistrationRequest {
// 第1段階: 必須チェック
@NotBlank(message = "ユーザーIDは必須です")
// 第2段階: 形式チェック
@Pattern(
regexp = "^[a-zA-Z][a-zA-Z0-9_]*$",
message = "ユーザーIDは英字で始まり、英数字とアンダースコアのみ使用できます",
groups = First.class
)
// 第3段階: 長さチェック
@Size(min = 5, max = 20, message = "ユーザーIDは5文字以上20文字以下で入力してください", groups = Second.class)
private String userId;
// ゲッター、セッター
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
}
|
パスパラメータとクエリパラメータのバリデーション#
リクエストボディだけでなく、パスパラメータやクエリパラメータにもバリデーションを適用できます。この場合、コントローラークラスに@Validatedを付与します。
パラメータバリデーションの例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
package com.example.demoapi.controller;
import com.example.demoapi.dto.UserResponse;
import com.example.demoapi.service.UserService;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Positive;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/users")
@Validated // クラスレベルで@Validatedを付与
public class UserSearchController {
private final UserService userService;
public UserSearchController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(
@PathVariable @Positive(message = "IDは正の整数で指定してください") Long id) {
UserResponse response = userService.getUser(id);
return ResponseEntity.ok(response);
}
@GetMapping
public ResponseEntity<List<UserResponse>> searchUsers(
@RequestParam(defaultValue = "0") @Min(value = 0, message = "ページ番号は0以上で指定してください") int page,
@RequestParam(defaultValue = "20") @Min(value = 1, message = "ページサイズは1以上で指定してください")
@Max(value = 100, message = "ページサイズは100以下で指定してください") int size) {
List<UserResponse> users = userService.searchUsers(page, size);
return ResponseEntity.ok(users);
}
}
|
ConstraintViolationExceptionのハンドリング#
パスパラメータやクエリパラメータのバリデーションエラーはConstraintViolationExceptionとしてスローされます。グローバル例外ハンドラーにこの例外のハンドリングを追加します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
package com.example.demoapi.exception;
import com.example.demoapi.dto.ErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex,
HttpServletRequest request) {
List<ErrorResponse.FieldError> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new ErrorResponse.FieldError(
error.getField(),
error.getDefaultMessage(),
error.getRejectedValue()))
.toList();
ErrorResponse response = new ErrorResponse();
response.setStatus(HttpStatus.BAD_REQUEST.value());
response.setError("Validation Failed");
response.setMessage("入力値に誤りがあります");
response.setPath(request.getRequestURI());
response.setFieldErrors(fieldErrors);
return ResponseEntity.badRequest().body(response);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> handleConstraintViolationException(
ConstraintViolationException ex,
HttpServletRequest request) {
List<ErrorResponse.FieldError> fieldErrors = ex.getConstraintViolations()
.stream()
.map(violation -> {
String propertyPath = violation.getPropertyPath().toString();
// メソッド名を除去してパラメータ名のみを取得
String field = propertyPath.contains(".")
? propertyPath.substring(propertyPath.lastIndexOf(".") + 1)
: propertyPath;
return new ErrorResponse.FieldError(
field,
violation.getMessage(),
violation.getInvalidValue());
})
.toList();
ErrorResponse response = new ErrorResponse();
response.setStatus(HttpStatus.BAD_REQUEST.value());
response.setError("Validation Failed");
response.setMessage("パラメータに誤りがあります");
response.setPath(request.getRequestURI());
response.setFieldErrors(fieldErrors);
return ResponseEntity.badRequest().body(response);
}
}
|
バリデーション処理の流れ#
以下のシーケンス図は、Spring Boot REST APIにおけるバリデーション処理の流れを示しています。
sequenceDiagram
participant Client as クライアント
participant Filter as フィルター
participant Controller as コントローラー
participant Validator as Validator
participant Handler as 例外ハンドラー
participant Service as サービス
Client->>Filter: HTTPリクエスト
Filter->>Controller: リクエスト転送
Controller->>Validator: @Valid/@Validated による検証
alt バリデーション成功
Validator-->>Controller: 検証OK
Controller->>Service: ビジネスロジック実行
Service-->>Controller: 処理結果
Controller-->>Client: 200 OK / 201 Created
else バリデーション失敗
Validator-->>Controller: MethodArgumentNotValidException
Controller->>Handler: 例外をスロー
Handler-->>Client: 400 Bad Request + エラー詳細
endまとめ#
本記事では、Spring Boot REST APIにおけるBean Validationの活用方法を解説しました。重要なポイントを振り返ります。
- 標準アノテーションの活用:
@NotNull、@Size、@Emailなどの標準アノテーションで多くの検証要件をカバーできます
- カスタムバリデーション: 標準アノテーションで対応できない場合は、独自のアノテーションとバリデータを作成します
- グループバリデーション: 操作(登録・更新など)ごとに異なる検証ルールを適用できます
- 適切なエラーハンドリング:
@RestControllerAdviceを使って統一されたエラーレスポンスを返却します
Bean Validationを適切に活用することで、堅牢で保守性の高いREST APIを構築できます。次のステップとして、バリデーションエラーのテスト方法や、非同期バリデーションの実装について学習することをお勧めします。
参考リンク#