Hibernate Validator
Library
Hibernate Validator
Overview
Hibernate Validator is the reference implementation of Java Bean Validation (JSR 380) and serves as the de facto standard for validation in Java applications. It provides annotation-based declarative validation and is adopted as standard by major frameworks including Spring Boot, Jakarta EE, and Hibernate. As of 2025, it supports Jakarta Bean Validation 3.1.1 and Jakarta EE 11, with the latest 9.0 series requiring Java 17 or higher for full compatibility with modern Java ecosystems.
Details
Hibernate Validator 9.0 is the latest release in 2025, positioned as the reference implementation for Jakarta EE 11. In addition to Bean Validation 2.0 (JSR 380) specifications, it implements Jakarta Validation 3.1.1 new features, providing powerful capabilities such as container element validation, constraint application to type parameters, and new built-in constraints. The 2025 version adds new constraints addressing modern validation needs such as Korean Resident Registration Numbers (@KorRRN) and Bitcoin addresses (@BitcoinAddress), with enhanced JPMS (Java Platform Module System) support.
Key Features
- Declarative Validation: Clear and readable validation definition through annotations
- Jakarta EE Integration: Complete Jakarta EE 11 support for enterprise application development
- Rich Built-in Constraints: Over 50 validation constraints including @NotNull, @Size, @Email
- Custom Validators: Flexible extension capabilities for custom business rules
- Internationalization Support: Multi-language message support and locale-specific validation
- Container Validation: Generic type element validation like List<@Valid User>
Pros and Cons
Pros
- Industry standard for Java enterprise application development
- High compatibility through standard adoption in Spring Boot and Jakarta EE
- Improved maintainability and code readability through declarative descriptions
- Extensive documentation and community support
- Performance-optimized mature library
- Multi-language support through message internationalization
Cons
- Java-only with no cross-platform support
- Complex configuration for sophisticated validation scenarios
- Relatively high learning curve (requires understanding Bean Validation specifications)
- Heavy dependencies (Jakarta EE environment prerequisite)
- Difficult implementation of dynamic validation rules
- Consider alternatives for extremely high-performance requirements
Reference Pages
Code Examples
Installation and Basic Setup
<!-- Maven dependency -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>9.0.1.Final</version>
</dependency>
<!-- Expression Language implementation (if needed) -->
<dependency>
<groupId>org.glassfish.expressly</groupId>
<artifactId>expressly</artifactId>
<version>6.0.0</version>
</dependency>
<!-- CDI environment integration (optional) -->
<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'
}
Basic Entity Validation
import jakarta.validation.constraints.*;
import jakarta.validation.Valid;
import java.time.LocalDate;
import java.util.List;
public class User {
@NotNull(message = "ID is required")
private Long id;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;
@Email(message = "Please enter a valid email address")
@NotBlank
private String email;
@Min(value = 18, message = "Age must be at least 18")
@Max(value = 120, message = "Age must be at most 120")
private Integer age;
@Past(message = "Birth date must be in the past")
private LocalDate birthDate;
@Pattern(regexp = "^\\d{3}-\\d{3}-\\d{4}$", message = "Phone number format is incorrect (e.g., 123-456-7890)")
private String phoneNumber;
// Nested object validation
@Valid
@NotNull
private Address address;
// Container element validation (Bean Validation 2.0+)
@NotEmpty(message = "At least one tag is required")
private List<@NotBlank String> tags;
// Constructor, getters, setters
public User() {}
public User(String name, String email, Integer age) {
this.name = name;
this.email = email;
this.age = age;
}
// Getters and setters omitted
}
public class Address {
@NotBlank(message = "Zip code is required")
@Pattern(regexp = "^\\d{5}(-\\d{4})?$", message = "Zip code format is incorrect (e.g., 12345 or 12345-6789)")
private String zipCode;
@NotBlank(message = "State is required")
private String state;
@NotBlank(message = "City is required")
private String city;
@NotBlank(message = "Street is required")
private String street;
// Constructor, getters, setters omitted
}
Validation Execution and Detailed Error Handling
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) {
// Test with invalid data
User invalidUser = new User();
invalidUser.setName(""); // Empty string
invalidUser.setEmail("invalid-email"); // Invalid email
invalidUser.setAge(15); // Under 18
invalidUser.setPhoneNumber("12345"); // Invalid phone format
// Execute validation
Set<ConstraintViolation<User>> violations = validator.validate(invalidUser);
if (!violations.isEmpty()) {
System.out.println("Validation errors occurred:");
for (ConstraintViolation<User> violation : violations) {
System.out.printf("Property: %s%n", violation.getPropertyPath());
System.out.printf("Invalid value: %s%n", violation.getInvalidValue());
System.out.printf("Message: %s%n", violation.getMessage());
System.out.println("---");
}
}
// Test with valid data
User validUser = new User();
validUser.setId(1L);
validUser.setName("John Doe");
validUser.setEmail("[email protected]");
validUser.setAge(30);
validUser.setBirthDate(LocalDate.of(1993, 5, 15));
validUser.setPhoneNumber("123-456-7890");
Address address = new Address();
address.setZipCode("12345");
address.setState("California");
address.setCity("Los Angeles");
address.setStreet("123 Main St");
validUser.setAddress(address);
validUser.setTags(List.of("developer", "java"));
Set<ConstraintViolation<User>> validViolations = validator.validate(validUser);
if (validViolations.isEmpty()) {
System.out.println("Validation successful!");
}
}
// Validate specific property only
public static void validateProperty() {
User user = new User();
Set<ConstraintViolation<User>> emailViolations =
validator.validateProperty(user, "email");
if (!emailViolations.isEmpty()) {
System.out.println("Email has errors:");
emailViolations.forEach(violation ->
System.out.println(violation.getMessage()));
}
}
}
Spring Boot Integration and Custom Validators
// Spring Boot Controller validation
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);
}
// User creation logic
return ResponseEntity.ok("User created successfully");
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable @Positive Long id) {
// ID retrieval logic
return ResponseEntity.ok(new User());
}
}
// Custom validator annotation definition
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = USPhoneNumberValidator.class)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface USPhoneNumber {
String message() default "US phone number format is incorrect";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// Custom validator implementation
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
public class USPhoneNumberValidator implements ConstraintValidator<USPhoneNumber, String> {
private static final Pattern PHONE_PATTERN = Pattern.compile(
"^\\(?([0-9]{3})\\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$"
);
@Override
public void initialize(USPhoneNumber constraintAnnotation) {
// Initialization logic if needed
}
@Override
public boolean isValid(String phoneNumber, ConstraintValidatorContext context) {
if (phoneNumber == null) {
return true; // null permission controlled by @NotNull
}
boolean isValid = PHONE_PATTERN.matcher(phoneNumber).matches();
if (!isValid) {
// Custom error message setting
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
"Phone number must be in US format (e.g., (123) 456-7890, 123-456-7890)"
).addConstraintViolation();
}
return isValid;
}
}
// Using custom validator
public class Contact {
@USPhoneNumber
private String phoneNumber;
// Other fields, constructor, getters, setters
}
Group Validation and Method Validation
// Validation group definition
public interface CreateGroup {}
public interface UpdateGroup {}
public class Product {
@NotNull(groups = UpdateGroup.class, message = "ID is required for updates")
private Long id;
@NotBlank(groups = {CreateGroup.class, UpdateGroup.class}, message = "Product name is required")
@Size(min = 1, max = 100, groups = {CreateGroup.class, UpdateGroup.class})
private String name;
@NotNull(groups = {CreateGroup.class, UpdateGroup.class}, message = "Price is required")
@PositiveOrZero(groups = {CreateGroup.class, UpdateGroup.class}, message = "Price must be zero or positive")
private BigDecimal price;
@NotBlank(groups = CreateGroup.class, message = "Category is required for creation")
private String category;
// Constructor, getters, setters
}
// Group validation execution
@Service
@Validated
public class ProductService {
@Autowired
private Validator validator;
// Method parameter validation
public Product createProduct(@Validated(CreateGroup.class) Product product) {
// Creation logic
return product;
}
public Product updateProduct(@Validated(UpdateGroup.class) Product product) {
// Update logic
return product;
}
// Method return value validation
@Valid
public Product getProductById(@NotNull @Positive Long id) {
// Product retrieval logic
return new Product();
}
// Manual group validation
public void validateProductForCreate(Product product) {
Set<ConstraintViolation<Product>> violations =
validator.validate(product, CreateGroup.class);
if (!violations.isEmpty()) {
StringBuilder sb = new StringBuilder("Validation errors:\n");
violations.forEach(violation ->
sb.append(violation.getPropertyPath())
.append(": ")
.append(violation.getMessage())
.append("\n"));
throw new IllegalArgumentException(sb.toString());
}
}
}
Internationalization and Error Message Customization
// messages.properties (default - English)
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 (Japanese)
user.name.notblank=名前は必須です
user.email.invalid=有効なメールアドレスを入力してください
user.age.range=年齢は{min}歳以上{max}歳以下で入力してください
// messages_es.properties (Spanish)
user.name.notblank=El nombre es obligatorio
user.email.invalid=Por favor ingrese una dirección de correo válida
user.age.range=La edad debe estar entre {min} y {max}
// Entity using custom messages
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;
// Getters, setters
}
// Programmatic message internationalization
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 -> {
// Display message in specified locale
String message = violation.getMessage();
System.out.println(message);
});
}
}
Advanced Integration in Jakarta EE Environment
// Validator injection in CDI environment
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 validation integration
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 validation is automatically executed
// 400 error is automatically returned if validation errors exist
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();
}
}
// Entity validation in JPA environment
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 = "Username is required")
@Size(min = 2, max = 100)
private String username;
@Column(nullable = false, unique = true, length = 255)
@Email(message = "Please enter a valid email address")
@NotBlank
private String email;
@Column(nullable = false)
@PastOrPresent(message = "Creation date must be present or past")
private LocalDateTime createdAt;
// JPA lifecycle callback
@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("Entity validation error: ");
violations.forEach(violation ->
sb.append(violation.getPropertyPath())
.append("=")
.append(violation.getMessage())
.append("; "));
throw new IllegalStateException(sb.toString());
}
}
// Constructor, getters, setters
}