Hibernate Validation focuses on data quality before invalid state reaches persistence. The unique problem is Bean Validation constraints, custom validators, groups, lifecycle timing, and error messages.
This page should teach validation gates, not Hibernate setup. Request DTOs, entities, and database constraints each protect a different layer.
Validation can run on incoming request DTOs, before entity insert/update, or through an explicit Validator call. The timing determines which errors users see and which rules protect persistence.
Built-in annotations cover common rules. Custom constraints express rules that belong to the domain, such as strong passwords, SKU format, or age eligibility.
Create and update workflows may require different rules. Validation groups handle these differences, but too many groups make models hard to reason about.
Hibernate Validator is the reference implementation of Jakarta Bean Validation (formerly javax.validation). It allows you to declare constraints directly on entity fields using annotations. Spring Boot includes it automatically via spring-boot-starter-validation.
package com.example.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull(message = "Name is required")
@NotBlank(message = "Name cannot be blank")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;
@NotNull(message = "Email is required")
@Email(message = "Email must be a valid email address")
@Column(unique = true)
private String email;
@NotNull(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
@Pattern(regexp = "^(?=.*[A-Z])(?=.*[0-9]).+$",
message = "Password must contain at least one uppercase letter and one digit")
private String password;
@Min(value = 18, message = "Age must be at least 18")
@Max(value = 120, message = "Age must be at most 120")
private Integer age;
@DecimalMin(value = "0.0", inclusive = false, message = "Salary must be positive")
@DecimalMax(value = "999999.99", message = "Salary exceeds maximum")
@Digits(integer = 6, fraction = 2, message = "Invalid salary format")
private Double salary;
@NotNull
@Positive(message = "Score must be positive")
private Integer score;
@Past(message = "Birth date must be in the past")
private java.time.LocalDate birthDate;
@Future(message = "Expiry date must be in the future")
private java.time.LocalDate expiryDate;
@AssertTrue(message = "Terms must be accepted")
private boolean termsAccepted;
// getters/setters...
}
package com.example.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
// Step 1: Define the annotation
@Documented
@Constraint(validatedBy = UsernameValidator.class) // Link to validator class
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidUsername {
String message() default "Username must be 3-20 chars, alphanumeric and underscores only";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// Usage on entity field:
// @ValidUsername
// private String username;
package com.example.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
// Step 2: Implement the validator logic
public class UsernameValidator
implements ConstraintValidator<ValidUsername, String> {
private static final String USERNAME_PATTERN = "^[a-zA-Z0-9_]{3,20}$";
@Override
public void initialize(ValidUsername annotation) {
// Called once before validation - can read annotation attributes here
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true; // Let @NotNull handle null check
if (!value.matches(USERNAME_PATTERN)) {
// Optionally customize the error message
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
"Username '" + value + "' is invalid. Use 3-20 alphanumeric chars.")
.addConstraintViolation();
return false;
}
return true;
}
}
package com.example.controller;
import com.example.model.User;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.validation.FieldError;
import java.util.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
// @Valid triggers Bean Validation on the @RequestBody
// If validation fails, Spring throws MethodArgumentNotValidException
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
// If we reach here, validation passed
return ResponseEntity.status(201).body(userService.save(user));
}
// Global exception handler for validation errors
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new LinkedHashMap<>();
// Collect all field errors
ex.getBindingResult().getAllErrors().forEach(error -> {
String field = ((FieldError) error).getField();
String message = error.getDefaultMessage();
errors.put(field, message);
});
return ResponseEntity.badRequest().body(errors);
}
}
// Example error response for invalid input:
// {
// "name": "Name must be between 2 and 50 characters",
// "email": "Email must be a valid email address",
// "age": "Age must be at least 18"
// }
public class RegistrationRequest {
@NotBlank @Size(min = 3, max = 40)
private String username;
@Email @NotBlank
private String email;
@StrongPassword
private String password;
}
Usually, yes. A request DTO exposes only the fields that clients may change, while the entity keeps persistence details such as IDs and relationships private.
@NotNull is application-level validation and can be skipped by another service, bulk SQL, disabled validation, or direct database access. A database NOT NULL constraint protects every write path. Use both when the rule is fundamental: validation gives earlier, friendlier feedback, while the database guarantees integrity.
Groups help when the same model has different rules for create, update, publish, or administrative operations.
Explore 500+ free tutorials across 20+ languages and frameworks.