Bean Validation (Jakarta Bean Validation)

バリデーションライブラリJavaアノテーションエンタープライズAPI検証Jakarta EE

バリデーションライブラリ

Bean Validation (Jakarta Bean Validation)

概要

Jakarta Bean Validation(旧Java Bean Validation)は、Javaアプリケーション向けの標準的なバリデーションフレームワークです。アノテーションベースのアプローチで、Beanのプロパティが特定の基準を満たすことを保証します。Jakarta EEおよびJava SEの一部として、エンタープライズアプリケーションやAPIバリデーションに広く使用されています。

詳細

Jakarta Bean Validationは、オブジェクトレベルの制約宣言とバリデーション機能を提供します。@NotNull、@Min、@Maxなどのアノテーションを使用して、宣言的にバリデーションルールを定義できます。バージョン3.0からはjakartaパッケージ名前空間に移行し、3.1では最小Java要件がJava 17に更新されました。

主要な特徴:

  • 標準バリデーションアノテーション: @NotNull、@Size、@Min、@Max、@Email、@Pattern等の豊富な組み込み制約
  • カスタム制約: 独自のバリデーションロジックを実装可能
  • グループバリデーション: 部分的なバリデーションや順序制御が可能
  • メソッドバリデーション: パラメータと戻り値のバリデーション(1.1以降)
  • カスケーディング: @Validによる関連オブジェクトの再帰的バリデーション
  • コンテナ要素バリデーション: List<@Valid Employee>のような型引数のバリデーション(2.0以降)

Hibernate Validatorが参照実装として提供され、Spring BootやJakarta EEコンテナとシームレスに統合されます。

メリット・デメリット

メリット

  • 標準化: Java EE/Jakarta EEの標準仕様として業界標準
  • 宣言的アプローチ: アノテーションによる簡潔で読みやすいコード
  • 統合性: Spring Boot、JAX-RS、JPAなど主要フレームワークとの優れた統合
  • 拡張性: カスタムバリデータの作成が容易
  • 国際化対応: エラーメッセージの多言語サポート
  • パフォーマンス: 効率的なバリデーション実行

デメリット

  • 学習曲線: グループバリデーションなど高度な機能の理解に時間が必要
  • アノテーション依存: 過度の使用によりコードが冗長になる可能性
  • 複雑なバリデーション: ビジネスロジックが複雑な場合、カスタムバリデータが増える
  • バージョン移行: javax→jakarta名前空間への移行作業が必要(Spring Boot 3以降)

参考ページ

書き方の例

基本的なバリデーション

import jakarta.validation.constraints.*;

public class User {
    @NotNull(message = "名前は必須です")
    @Size(min = 2, max = 50, message = "名前は2文字以上50文字以下で入力してください")
    private String name;
    
    @NotNull(message = "メールアドレスは必須です")
    @Email(message = "有効なメールアドレスを入力してください")
    private String email;
    
    @Min(value = 18, message = "年齢は18歳以上である必要があります")
    @Max(value = 120, message = "年齢は120歳以下である必要があります")
    private int age;
    
    @Pattern(regexp = "^\\d{3}-\\d{4}$", 
             message = "郵便番号は XXX-XXXX 形式で入力してください")
    private String postalCode;
    
    // getters and setters
}

カスタムバリデータの作成

// カスタムアノテーション
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
@Documented
public @interface PhoneNumber {
    String message() default "無効な電話番号です";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    
    String countryCode() default "JP";
}

// バリデータ実装
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
    private String countryCode;
    
    @Override
    public void initialize(PhoneNumber annotation) {
        this.countryCode = annotation.countryCode();
    }
    
    @Override
    public boolean isValid(String phoneNumber, ConstraintValidatorContext context) {
        if (phoneNumber == null) {
            return true; // @NotNullと組み合わせて使用
        }
        
        // 日本の電話番号形式をチェック
        if ("JP".equals(countryCode)) {
            return phoneNumber.matches("^0\\d{1,4}-\\d{1,4}-\\d{4}$") ||
                   phoneNumber.matches("^0\\d{9,10}$");
        }
        
        return false;
    }
}

グループバリデーション

// バリデーショングループの定義
public interface BasicInfo {}
public interface AdvancedInfo {}

public class Employee {
    @NotNull(groups = BasicInfo.class)
    private String name;
    
    @NotNull(groups = BasicInfo.class)
    @Email(groups = BasicInfo.class)
    private String email;
    
    @NotNull(groups = AdvancedInfo.class)
    @Size(min = 10, max = 10, groups = AdvancedInfo.class)
    private String employeeId;
    
    @Min(value = 0, groups = AdvancedInfo.class)
    @Max(value = 1000000, groups = AdvancedInfo.class)
    private BigDecimal salary;
}

// 使用例
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

// 基本情報のみバリデーション
Set<ConstraintViolation<Employee>> basicViolations = 
    validator.validate(employee, BasicInfo.class);

// 詳細情報のみバリデーション
Set<ConstraintViolation<Employee>> advancedViolations = 
    validator.validate(employee, AdvancedInfo.class);

// すべてバリデーション
Set<ConstraintViolation<Employee>> allViolations = 
    validator.validate(employee, BasicInfo.class, AdvancedInfo.class);

カスケードバリデーション

public class Order {
    @NotNull
    private String orderId;
    
    @NotNull
    @Valid  // カスケードバリデーションを有効化
    private Customer customer;
    
    @NotEmpty
    @Valid  // リスト要素もバリデーション
    private List<@Valid OrderItem> items;
    
    @Valid
    private Map<@NotNull String, @Valid Product> productMap;
}

public class Customer {
    @NotNull
    @Size(min = 2, max = 100)
    private String name;
    
    @NotNull
    @Valid
    private Address address;
}

public class Address {
    @NotBlank
    private String street;
    
    @NotBlank
    @Pattern(regexp = "\\d{3}-\\d{4}")
    private String postalCode;
}

Spring Boot統合

// REST Controller での使用
@RestController
@RequestMapping("/api/users")
@Validated  // クラスレベルでバリデーション有効化
public class UserController {
    
    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        // @Validにより自動的にバリデーションが実行される
        // バリデーションエラーの場合、400 Bad Requestが返される
        return ResponseEntity.ok(userService.save(user));
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody User user) {
        return ResponseEntity.ok(userService.update(id, user));
    }
}

// エラーハンドリング
@RestControllerAdvice
public class ValidationExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return ResponseEntity.badRequest().body(errors);
    }
}

メソッドバリデーション

@Service
@Validated  // メソッドバリデーションを有効化
public class PaymentService {
    
    public void processPayment(
            @NotNull @Valid CreditCard card,
            @Min(1) @Max(1000000) BigDecimal amount) {
        // メソッドパラメータが自動的にバリデーションされる
    }
    
    @NotNull
    @Valid
    public Receipt completeTransaction(
            @NotNull String transactionId) {
        // 戻り値もバリデーション可能
        return new Receipt();
    }
}

// 設定(Spring Boot では自動設定)
@Configuration
@EnableMethodValidation
public class ValidationConfig {
    
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}