package com.example.demo.dto;
import com.example.demo.validation.UniqueEmail;
import jakarta.validation.constraints.*;
import lombok.Data;
@Data
public class CreateUserRequest {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
@UniqueEmail
private String email;
@Pattern(regexp = "^\+?[1-9]\d{1,14}$", message = "Invalid phone number")
private String phone;
@Min(value = 18, message = "Must be at least 18 years old")
@Max(value = 120, message = "Age cannot exceed 120")
private Integer age;
@AssertTrue(message = "Must accept terms and conditions")
private Boolean termsAccepted;
}
package com.example.demo.validation;
import com.example.demo.repository.UserRepository;
import jakarta.validation.Constraint;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
String message() default "Email already exists";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
private final UserRepository userRepository;
public UniqueEmailValidator(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public boolean isValid(String email, ConstraintValidatorContext context) {
if (email == null) {
return true; // Let @NotNull handle null validation
}
return !userRepository.existsByEmail(email);
}
}
package com.example.demo.controller;
import com.example.demo.dto.CreateUserRequest;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
@Validated
public class ValidationController {
private final UserService userService;
public ValidationController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) {
User user = new User();
user.setName(request.getName());
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
User created = userService.save(user);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@GetMapping("/validate")
public ResponseEntity<String> validateParam(
@RequestParam @Email(message = "Invalid email format") String email
) {
return ResponseEntity.ok("Valid email: " + email);
}
}
Bean Validation (JSR 380) validates objects using annotations. Common constraints include @NotNull, @NotBlank, @Size, @Email, @Min, @Max, and @Pattern. I apply annotations to fields, methods, or parameters. @Valid triggers validation in Spring controllers. Custom validators extend ConstraintValidator for business rules. Groups enable conditional validation. ConstraintViolation contains error details. Validation happens before data reaches service layer, ensuring data integrity. Spring's @Validated supports method-level validation in services. Error messages are customizable via properties files or annotation parameters. Validation prevents invalid data from persisting, reducing bugs and improving security. The declarative approach keeps validation logic close to domain models.