This project demonstrates comprehensive validation features in Java and Spring Boot, including Bean Validation (JSR-303/JSR-380), Spring Boot validation, custom validators, validation groups, and Lombok integration for reduced boilerplate code.
- Project Structure
- Validation Features
- Lombok Integration
- Built-in Validation Annotations
- Custom Validation
- Validation Groups
- Testing the Application
- Best Practices
validation/
├── src/main/java/com/noahcoy/validation/
│ ├── ValidationApplication.java # Main Spring Boot application
│ ├── annotation/
│ │ └── ValidUsername.java # Custom validation annotation
│ ├── controller/
│ │ ├── UserController.java # REST endpoints with validation
│ │ ├── ValidationDemoController.java # Demo endpoints
│ │ └── LombokDemoController.java # Lombok demonstration endpoints
│ ├── exception/
│ │ └── GlobalExceptionHandler.java # Global validation error handling
│ ├── group/
│ │ └── ValidationGroups.java # Validation group interfaces
│ ├── model/
│ │ ├── User.java # Basic model with Lombok
│ │ ├── UserWithGroups.java # Model with validation groups + Lombok
│ │ ├── Address.java # Address model with Lombok features
│ │ └── UserWithAddress.java # Nested validation with Lombok
│ ├── service/
│ │ └── ValidationService.java # Programmatic validation
│ └── validator/
│ └── UsernameValidator.java # Custom validator implementation
├── pom.xml # Maven dependencies (includes Lombok)
└── README.md # This file
Standard Java validation annotations for common validation scenarios.
- Automatic validation with
@Valid - Method-level validation with
@Validated - Global error handling
- Custom annotation:
@ValidUsername - Complex business rule validation
- Contextual error messages
- Different validation rules for different operations
- Create, Update, Admin, and Basic groups
- Conditional validation based on context
- Manual validation using
Validatorinterface - Property-specific validation
- Value validation without object instances
This project extensively uses Lombok to reduce boilerplate code while maintaining full validation functionality.
Generates getters, setters, toString, equals, and hashCode methods:
@Data
public class User {
@NotBlank(message = "Username cannot be blank")
private String username;
// No need for getters/setters - Lombok generates them!
}Provides fluent builder pattern for object creation:
@Builder
public class User {
// Fields with validation annotations
}
// Usage:
User user = User.builder()
.username("johnsmith")
.email("john@example.com")
.age(25)
.build();Generates constructors automatically:
@NoArgsConstructor // Empty constructor
@AllArgsConstructor // Constructor with all fields
public class User {
// Fields
}Custom toString exclusion (for security):
@ToString(exclude = {"password"}) // Password won't appear in toString()
public class User {
private String password;
}Custom equals/hashCode logic:
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Address {
@EqualsAndHashCode.Include
private Long id; // Only ID used for equals/hashCode
private String street; // Not included in equals/hashCode
}Custom getter/setter access levels:
@Getter(AccessLevel.NONE) // No getter generated
@Setter(AccessLevel.PROTECTED) // Protected setter
private String internalNotes;Builder defaults:
@Builder.Default
private LocalDateTime registrationDate = LocalDateTime.now();- Reduced Code: 60-80% less boilerplate code
- Validation Preserved: All validation annotations work seamlessly
- Builder Pattern: Clean object creation with validation
- Security: Easy exclusion of sensitive fields from toString()
- Maintainability: Changes to fields automatically update generated methods
| Annotation | Purpose | Example |
|---|---|---|
@NotNull |
Field must not be null | @NotNull private Long id; |
@NotEmpty |
Collection/array must not be null or empty | @NotEmpty private List<String> items; |
@NotBlank |
String must not be null, empty, or whitespace | @NotBlank private String username; |
@Size(min, max) |
Size must be within bounds | @Size(min=3, max=20) private String name; |
@Min(value) |
Number must be >= value | @Min(18) private Integer age; |
@Max(value) |
Number must be <= value | @Max(120) private Integer age; |
@DecimalMin(value) |
Decimal must be >= value | @DecimalMin("0.0") private Double salary; |
@DecimalMax(value) |
Decimal must be <= value | @DecimalMax("999999.99") private Double salary; |
@Pattern(regexp) |
String must match regex | @Pattern(regexp="^[a-zA-Z0-9_]+$") private String username; |
@Email |
String must be valid email | @Email private String email; |
@Past |
Date must be in the past | @Past private LocalDate birthDate; |
@Future |
Date must be in the future | @Future private LocalDate expiryDate; |
@Positive |
Number must be positive | @Positive private Integer count; |
@Negative |
Number must be negative | @Negative private Integer deficit; |
@Documented
@Constraint(validatedBy = UsernameValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidUsername {
String message() default "Username must be unique and follow the required format";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}public class UsernameValidator implements ConstraintValidator<ValidUsername, String> {
@Override
public boolean isValid(String username, ConstraintValidatorContext context) {
// Custom validation logic
// Return true if valid, false if invalid
}
}Validation groups allow different validation rules for different scenarios:
public class ValidationGroups {
public interface Create {} // For user creation
public interface Update {} // For user updates
public interface Admin {} // For admin operations
public interface Basic {} // For basic validation
}Usage in controller:
@PostMapping("/with-groups")
public ResponseEntity<String> createUser(
@Validated({ValidationGroups.Create.class, ValidationGroups.Basic.class})
@RequestBody UserWithGroups user) {
// Only validates Create and Basic groups
}cd validation
./mvnw spring-boot:run# Valid user
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{
"id": 1,
"username": "johndoe",
"email": "john@example.com",
"password": "Password123",
"age": 25
}'
# Invalid user (multiple validation errors)
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{
"id": 0,
"username": "ab",
"email": "invalid-email",
"password": "weak",
"age": 15
}'# Demonstrate Lombok builder pattern
curl -X POST http://localhost:8080/api/lombok-demo/builder-demo# Create user with nested address validation
curl -X POST http://localhost:8080/api/lombok-demo/with-address \
-H "Content-Type: application/json" \
-d '{
"username": "janesmith",
"email": "jane@example.com",
"password": "SecurePass123",
"age": 28,
"primaryAddress": {
"id": 1,
"street": "123 Main St",
"city": "New York",
"state": "NY",
"zipCode": "10001",
"country": "US",
"verified": true
}
}'# Demonstrate Lombok equals and hashCode
curl -X GET http://localhost:8080/api/lombok-demo/equals-demo# Get information about Lombok features
curl -X GET http://localhost:8080/api/lombok-demo/lombok-features# Create with groups (missing password - should fail)
curl -X POST http://localhost:8080/api/users/with-groups \
-H "Content-Type: application/json" \
-d '{
"username": "testuser",
"email": "test@example.com",
"age": 25
}'
# Update with groups (password not required)
curl -X PUT http://localhost:8080/api/users/with-groups/1 \
-H "Content-Type: application/json" \
-d '{
"username": "updateduser",
"email": "updated@example.com",
"age": 26
}'# Invalid ID (should return validation error)
curl -X GET http://localhost:8080/api/users/0
# Valid ID
curl -X GET http://localhost:8080/api/users/1# Empty username (should return validation error)
curl -X GET "http://localhost:8080/api/users/search?username="
# Valid username
curl -X GET "http://localhost:8080/api/users/search?username=john"# Invalid parameters
curl -X POST "http://localhost:8080/api/users/validate-method?name=&age=15"
# Valid parameters
curl -X POST "http://localhost:8080/api/users/validate-method?name=John&age=25"# Reserved username (should fail custom validation)
curl -X POST http://localhost:8080/api/users/with-groups \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"email": "admin@example.com",
"password": "Password123",
"age": 25
}'# Run validation demonstrations
curl -X GET http://localhost:8080/api/validation-demo/demonstrate
# Get validation features information
curl -X GET http://localhost:8080/api/validation-demo/featuresWhen validation fails, the application returns structured error responses:
{
"timestamp": "2024-01-15T10:30:00",
"status": 400,
"error": "Validation Failed",
"message": "Request contains invalid data",
"errors": [
{
"field": "username",
"message": "Username must be between 3 and 20 characters",
"rejectedValue": "ab"
},
{
"field": "email",
"message": "Email should be valid",
"rejectedValue": "invalid-email"
}
],
"path": "Validation Error"
}- Use Appropriate Annotations: Choose the right validation annotation for each field type
- Clear Error Messages: Provide user-friendly error messages
- Validation Groups: Use groups for different scenarios (create, update, admin)
- Custom Validators: Create custom validators for complex business rules
- Global Error Handling: Use
@ControllerAdvicefor consistent error responses - Multi-Layer Validation: Validate at multiple layers (client, server, database)
- Nested Validation: Use
@Validfor nested object validation - Separation of Concerns: Keep validation logic separate from business logic
- Lombok Best Practices:
- Use
@Datafor simple DTOs and entities - Exclude sensitive fields from
toStringusing@ToString(exclude = {...}) - Use
@EqualsAndHashCode.Includefor custom equality logic - Combine with validation annotations for robust models
- Use
@Builderfor complex object creation - Consider
@Valuefor immutable objects
- Use
The project uses the following key dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>- Bean Validation Specification
- Spring Boot Validation Documentation
- Hibernate Validator Documentation
- Project Lombok Documentation
- Lombok Features
- Add more custom validators (e.g., phone number format, credit card validation)
- Implement conditional validation (validation depends on other field values)
- Add internationalization (i18n) for validation messages
- Create validation for file uploads
- Implement cross-field validation (password confirmation)
- Add validation for nested objects and collections
- Lombok Exercises:
- Create an immutable model using
@Value - Implement a custom builder with validation
- Use
@Withfor creating modified copies of immutable objects - Experiment with
@Accessorsfor fluent setters
- Create an immutable model using