Hibernate Validator

バリデーションライブラリJavaBean ValidationJSR 380Jakarta EEアノテーション

ライブラリ

Hibernate Validator

概要

Hibernate ValidatorはJava Bean Validation(JSR 380)の参照実装であり、Javaアプリケーションにおけるバリデーションのデファクトスタンダードです。アノテーションベースの宣言的バリデーションを提供し、Spring Boot、Jakarta EE、Hibernateなどの主要フレームワークで標準採用されています。2025年現在、Jakarta Bean Validation 3.1.1とJakarta EE 11に対応し、最新版9.0系ではJava 17以上を要求する最新のJavaエコシステムに完全対応しています。

詳細

Hibernate Validator 9.0は2025年最新のリリースで、Jakarta EE 11の参照実装として位置づけられています。Bean Validation 2.0(JSR 380)の仕様に加え、Jakarta Validation 3.1.1の新機能を実装し、コンテナ要素のバリデーション、型引数への制約適用、新しい組み込み制約などの強力な機能を提供します。2025年版では韓国住民登録番号(@KorRRN)やビットコインアドレス(@BitcoinAddress)など、現代的なバリデーション需要に対応した新制約を追加し、JPMS(Java Platform Module System)サポートも強化されています。

主な特徴

  • 宣言的バリデーション: アノテーションによる明確で読みやすいバリデーション定義
  • Jakarta EE統合: Jakarta EE 11完全対応と企業向けアプリケーション開発支援
  • 豊富な組み込み制約: @NotNull、@Size、@Email等50以上のバリデーション制約
  • カスタムバリデーター: 独自のビジネスルールに対応する柔軟な拡張機能
  • 国際化対応: メッセージの多言語化と地域特化バリデーション
  • コンテナバリデーション: List<@Valid User>等のジェネリック型要素バリデーション

メリット・デメリット

メリット

  • Java企業アプリケーション開発の業界標準
  • Spring Boot、Jakarta EEでの標準採用による高い互換性
  • 宣言的記述による保守性とコードの可読性向上
  • 豊富なドキュメントとコミュニティサポート
  • パフォーマンスが最適化された成熟したライブラリ
  • メッセージの国際化機能による多言語対応

デメリット

  • Java専用でクロスプラットフォーム対応なし
  • 複雑なバリデーションでは設定が複雑になる
  • 学習コストが比較的高い(Bean Validation仕様の理解が必要)
  • 依存関係が重い(Jakarta EE環境が前提)
  • 動的バリデーションルールの実装が困難
  • パフォーマンス要求が極めて高い場合は他選択肢を検討

参考ページ

書き方の例

インストールと基本セットアップ

<!-- Maven dependency -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>9.0.1.Final</version>
</dependency>

<!-- Expression Language実装(必要に応じて) -->
<dependency>
    <groupId>org.glassfish.expressly</groupId>
    <artifactId>expressly</artifactId>
    <version>6.0.0</version>
</dependency>

<!-- CDI環境での統合(任意) -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-cdi</artifactId>
    <version>9.0.1.Final</version>
</dependency>
// Gradle dependency
dependencies {
    implementation 'org.hibernate.validator:hibernate-validator:9.0.1.Final'
    implementation 'org.glassfish.expressly:expressly:6.0.0'
}

基本的なエンティティバリデーション

import jakarta.validation.constraints.*;
import jakarta.validation.Valid;
import java.time.LocalDate;
import java.util.List;

public class User {
    
    @NotNull(message = "IDは必須です")
    private Long id;
    
    @NotBlank(message = "名前は必須です")
    @Size(min = 2, max = 50, message = "名前は2文字以上50文字以下で入力してください")
    private String name;
    
    @Email(message = "有効なメールアドレスを入力してください")
    @NotBlank
    private String email;
    
    @Min(value = 18, message = "年齢は18歳以上である必要があります")
    @Max(value = 120, message = "年齢は120歳以下である必要があります")
    private Integer age;
    
    @Past(message = "生年月日は過去の日付である必要があります")
    private LocalDate birthDate;
    
    @Pattern(regexp = "^\\d{3}-\\d{4}-\\d{4}$", message = "電話番号の形式が正しくありません(例:090-1234-5678)")
    private String phoneNumber;
    
    // ネストしたオブジェクトのバリデーション
    @Valid
    @NotNull
    private Address address;
    
    // コンテナ要素のバリデーション(Bean Validation 2.0+)
    @NotEmpty(message = "タグは最低1つ必要です")
    private List<@NotBlank String> tags;
    
    // コンストラクタ、ゲッター、セッター
    public User() {}
    
    public User(String name, String email, Integer age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }
    
    // ゲッター・セッター省略
}

public class Address {
    
    @NotBlank(message = "郵便番号は必須です")
    @Pattern(regexp = "^\\d{3}-\\d{4}$", message = "郵便番号の形式が正しくありません(例:123-4567)")
    private String zipCode;
    
    @NotBlank(message = "都道府県は必須です")
    private String prefecture;
    
    @NotBlank(message = "市区町村は必須です")
    private String city;
    
    @NotBlank(message = "住所は必須です")
    private String street;
    
    // コンストラクタ、ゲッター、セッター省略
}

バリデーション実行と詳細エラーハンドリング

import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import java.util.Set;

public class ValidationExample {
    
    private static final Validator validator;
    
    static {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }
    
    public static void main(String[] args) {
        // 無効なデータでテスト
        User invalidUser = new User();
        invalidUser.setName(""); // 空文字
        invalidUser.setEmail("invalid-email"); // 無効なメール
        invalidUser.setAge(15); // 18歳未満
        invalidUser.setPhoneNumber("12345"); // 無効な電話番号形式
        
        // バリデーション実行
        Set<ConstraintViolation<User>> violations = validator.validate(invalidUser);
        
        if (!violations.isEmpty()) {
            System.out.println("バリデーションエラーが発生しました:");
            
            for (ConstraintViolation<User> violation : violations) {
                System.out.printf("プロパティ: %s%n", violation.getPropertyPath());
                System.out.printf("無効な値: %s%n", violation.getInvalidValue());
                System.out.printf("メッセージ: %s%n", violation.getMessage());
                System.out.println("---");
            }
        }
        
        // 正常なデータでテスト
        User validUser = new User();
        validUser.setId(1L);
        validUser.setName("田中太郎");
        validUser.setEmail("[email protected]");
        validUser.setAge(30);
        validUser.setBirthDate(LocalDate.of(1993, 5, 15));
        validUser.setPhoneNumber("090-1234-5678");
        
        Address address = new Address();
        address.setZipCode("123-4567");
        address.setPrefecture("東京都");
        address.setCity("渋谷区");
        address.setStreet("道玄坂1-2-3");
        validUser.setAddress(address);
        
        validUser.setTags(List.of("開発者", "Java"));
        
        Set<ConstraintViolation<User>> validViolations = validator.validate(validUser);
        
        if (validViolations.isEmpty()) {
            System.out.println("バリデーション成功!");
        }
    }
    
    // 特定のプロパティのみバリデーション
    public static void validateProperty() {
        User user = new User();
        
        Set<ConstraintViolation<User>> emailViolations = 
            validator.validateProperty(user, "email");
            
        if (!emailViolations.isEmpty()) {
            System.out.println("メールアドレスにエラーがあります:");
            emailViolations.forEach(violation -> 
                System.out.println(violation.getMessage()));
        }
    }
}

Spring Boot統合とカスタムバリデーター

// Spring Boot Controllerでのバリデーション
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;

@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
    
    @PostMapping
    public ResponseEntity<?> createUser(@Valid @RequestBody User user, BindingResult result) {
        if (result.hasErrors()) {
            Map<String, String> errors = new HashMap<>();
            result.getFieldErrors().forEach(error -> 
                errors.put(error.getField(), error.getDefaultMessage()));
            return ResponseEntity.badRequest().body(errors);
        }
        
        // ユーザー作成処理
        return ResponseEntity.ok("ユーザーが正常に作成されました");
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable @Positive Long id) {
        // ID取得処理
        return ResponseEntity.ok(new User());
    }
}

// カスタムバリデーター注釈の定義
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = JapanesePhoneNumberValidator.class)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface JapanesePhoneNumber {
    String message() default "日本の電話番号形式が正しくありません";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// カスタムバリデーターの実装
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;

public class JapanesePhoneNumberValidator implements ConstraintValidator<JapanesePhoneNumber, String> {
    
    private static final Pattern PHONE_PATTERN = Pattern.compile(
        "^(070|080|090)-\\d{4}-\\d{4}$|^0\\d{1,4}-\\d{1,4}-\\d{4}$"
    );
    
    @Override
    public void initialize(JapanesePhoneNumber constraintAnnotation) {
        // 初期化処理(必要に応じて)
    }
    
    @Override
    public boolean isValid(String phoneNumber, ConstraintValidatorContext context) {
        if (phoneNumber == null) {
            return true; // null許可は@NotNullで制御
        }
        
        boolean isValid = PHONE_PATTERN.matcher(phoneNumber).matches();
        
        if (!isValid) {
            // カスタムエラーメッセージの設定
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(
                "電話番号は日本の形式で入力してください(例:090-1234-5678、03-1234-5678)"
            ).addConstraintViolation();
        }
        
        return isValid;
    }
}

// カスタムバリデーターの使用
public class Contact {
    
    @JapanesePhoneNumber
    private String phoneNumber;
    
    // その他のフィールド、コンストラクタ、ゲッター、セッター
}

グループバリデーションとメソッドバリデーション

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

public class Product {
    
    @NotNull(groups = UpdateGroup.class, message = "更新時はIDが必要です")
    private Long id;
    
    @NotBlank(groups = {CreateGroup.class, UpdateGroup.class}, message = "商品名は必須です")
    @Size(min = 1, max = 100, groups = {CreateGroup.class, UpdateGroup.class})
    private String name;
    
    @NotNull(groups = {CreateGroup.class, UpdateGroup.class}, message = "価格は必須です")
    @PositiveOrZero(groups = {CreateGroup.class, UpdateGroup.class}, message = "価格は0以上である必要があります")
    private BigDecimal price;
    
    @NotBlank(groups = CreateGroup.class, message = "作成時はカテゴリが必要です")
    private String category;
    
    // コンストラクタ、ゲッター、セッター
}

// グループバリデーションの実行
@Service
@Validated
public class ProductService {
    
    @Autowired
    private Validator validator;
    
    // メソッドパラメータのバリデーション
    public Product createProduct(@Validated(CreateGroup.class) Product product) {
        // 作成処理
        return product;
    }
    
    public Product updateProduct(@Validated(UpdateGroup.class) Product product) {
        // 更新処理
        return product;
    }
    
    // メソッド戻り値のバリデーション
    @Valid
    public Product getProductById(@NotNull @Positive Long id) {
        // 商品取得処理
        return new Product();
    }
    
    // 手動でのグループバリデーション
    public void validateProductForCreate(Product product) {
        Set<ConstraintViolation<Product>> violations = 
            validator.validate(product, CreateGroup.class);
            
        if (!violations.isEmpty()) {
            StringBuilder sb = new StringBuilder("バリデーションエラー:\n");
            violations.forEach(violation -> 
                sb.append(violation.getPropertyPath())
                  .append(": ")
                  .append(violation.getMessage())
                  .append("\n"));
            throw new IllegalArgumentException(sb.toString());
        }
    }
}

国際化とエラーメッセージカスタマイズ

// messages.properties (デフォルト - 英語)
user.name.notblank=Name is required
user.email.invalid=Please enter a valid email address
user.age.range=Age must be between {min} and {max}

// messages_ja.properties (日本語)
user.name.notblank=名前は必須です
user.email.invalid=有効なメールアドレスを入力してください
user.age.range=年齢は{min}歳以上{max}歳以下で入力してください

// messages_en.properties (英語)
user.name.notblank=Name is required
user.email.invalid=Please enter a valid email address
user.age.range=Age must be between {min} and {max}

// カスタムメッセージを使用するエンティティ
public class InternationalUser {
    
    @NotBlank(message = "{user.name.notblank}")
    private String name;
    
    @Email(message = "{user.email.invalid}")
    private String email;
    
    @Min(value = 18, message = "{user.age.range}")
    @Max(value = 120, message = "{user.age.range}")
    private Integer age;
    
    // ゲッター、セッター
}

// プログラムによるメッセージ国際化
import jakarta.validation.MessageInterpolator;
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
import org.hibernate.validator.resourceloading.PlatformResourceBundleLocator;

public class InternationalValidation {
    
    public static void validateWithLocale(Object obj, Locale locale) {
        ValidatorFactory factory = Validation.byDefaultProvider()
            .configure()
            .messageInterpolator(new ResourceBundleMessageInterpolator(
                new PlatformResourceBundleLocator("messages")))
            .buildValidatorFactory();
            
        Validator validator = factory.getValidator();
        
        Set<ConstraintViolation<Object>> violations = validator.validate(obj);
        
        violations.forEach(violation -> {
            // 指定されたロケールでメッセージを表示
            String message = violation.getMessage();
            System.out.println(message);
        });
    }
}

Jakarta EE環境での高度な統合

// CDI環境でのバリデーターの注入
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.validation.Validator;

@ApplicationScoped
public class UserValidationService {
    
    @Inject
    private Validator validator;
    
    public boolean isValidUser(User user) {
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        return violations.isEmpty();
    }
    
    public List<String> getValidationErrors(User user) {
        return validator.validate(user)
                .stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.toList());
    }
}

// JAX-RSでのバリデーション統合
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.validation.Valid;

@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {
    
    @POST
    public Response createUser(@Valid User user) {
        // Javaのバリデーションが自動実行される
        // バリデーションエラーがあると400エラーが自動返却
        return Response.ok().entity("User created successfully").build();
    }
    
    @PUT
    @Path("/{id}")
    public Response updateUser(
            @PathParam("id") @Positive Long id,
            @Valid User user) {
        return Response.ok().entity("User updated successfully").build();
    }
}

// JPA環境でのエンティティバリデーション
import jakarta.persistence.*;
import jakarta.validation.constraints.*;

@Entity
@Table(name = "users")
public class UserEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, length = 100)
    @NotBlank(message = "ユーザー名は必須です")
    @Size(min = 2, max = 100)
    private String username;
    
    @Column(nullable = false, unique = true, length = 255)
    @Email(message = "有効なメールアドレスを入力してください")
    @NotBlank
    private String email;
    
    @Column(nullable = false)
    @PastOrPresent(message = "作成日は現在または過去の日付である必要があります")
    private LocalDateTime createdAt;
    
    // JPAライフサイクルコールバック
    @PrePersist
    @PreUpdate
    public void validateEntity() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Set<ConstraintViolation<UserEntity>> violations = validator.validate(this);
        
        if (!violations.isEmpty()) {
            StringBuilder sb = new StringBuilder("エンティティバリデーションエラー: ");
            violations.forEach(violation -> 
                sb.append(violation.getPropertyPath())
                  .append("=")
                  .append(violation.getMessage())
                  .append("; "));
            throw new IllegalStateException(sb.toString());
        }
    }
    
    // コンストラクタ、ゲッター、セッター
}