unit-test-bean-validation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Unit Testing Bean Validation and Custom Validators

Jakarta Bean Validation与自定义验证器的单元测试

Test validation annotations and custom validator implementations using JUnit 5. Verify constraint violations, error messages, and validation logic in isolation.
使用JUnit 5测试验证注解和自定义验证器实现,独立验证约束违例、错误消息和验证逻辑。

When to Use This Skill

适用场景

Use this skill when:
  • Testing Jakarta Bean Validation (@NotNull, @Email, @Min, etc.)
  • Testing custom @Constraint validators
  • Verifying constraint violation error messages
  • Testing cross-field validation logic
  • Want fast validation tests without Spring context
  • Testing complex validation scenarios and edge cases
在以下场景中使用本技能:
  • 测试Jakarta Bean Validation(@NotNull、@Email、@Min等)
  • 测试自定义@Constraint验证器
  • 验证约束违例的错误消息
  • 测试跨字段验证逻辑
  • 无需Spring上下文即可快速执行验证测试
  • 测试复杂验证场景与边缘情况

Setup: Bean Validation

环境搭建:Bean Validation

Maven

Maven

xml
<dependency>
  <groupId>jakarta.validation</groupId>
  <artifactId>jakarta.validation-api</artifactId>
</dependency>
<dependency>
  <groupId>org.hibernate.validator</groupId>
  <artifactId>hibernate-validator</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <scope>test</scope>
</dependency>
xml
<dependency>
  <groupId>jakarta.validation</groupId>
  <artifactId>jakarta.validation-api</artifactId>
</dependency>
<dependency>
  <groupId>org.hibernate.validator</groupId>
  <artifactId>hibernate-validator</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <scope>test</scope>
</dependency>

Gradle

Gradle

kotlin
dependencies {
  implementation("jakarta.validation:jakarta.validation-api")
  testImplementation("org.hibernate.validator:hibernate-validator")
  testImplementation("org.junit.jupiter:junit-jupiter")
  testImplementation("org.assertj:assertj-core")
}
kotlin
dependencies {
  implementation("jakarta.validation:jakarta.validation-api")
  testImplementation("org.hibernate.validator:hibernate-validator")
  testImplementation("org.junit.jupiter:junit-jupiter")
  testImplementation("org.assertj:assertj-core")
}

Basic Pattern: Testing Validation Constraints

基础模式:测试验证约束

Setup Validator

初始化Validator

java
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import jakarta.validation.Validation;
import jakarta.validation.ConstraintViolation;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;

class UserValidationTest {

  private Validator validator;

  @BeforeEach
  void setUp() {
    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    validator = factory.getValidator();
  }

  @Test
  void shouldPassValidationWithValidUser() {
    User user = new User("Alice", "alice@example.com", 25);
    
    Set<ConstraintViolation<User>> violations = validator.validate(user);
    
    assertThat(violations).isEmpty();
  }

  @Test
  void shouldFailValidationWhenNameIsNull() {
    User user = new User(null, "alice@example.com", 25);
    
    Set<ConstraintViolation<User>> violations = validator.validate(user);
    
    assertThat(violations)
      .hasSize(1)
      .extracting(ConstraintViolation::getMessage)
      .contains("must not be blank");
  }
}
java
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import jakarta.validation.Validation;
import jakarta.validation.ConstraintViolation;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;

class UserValidationTest {

  private Validator validator;

  @BeforeEach
  void setUp() {
    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    validator = factory.getValidator();
  }

  @Test
  void shouldPassValidationWithValidUser() {
    User user = new User("Alice", "alice@example.com", 25);
    
    Set<ConstraintViolation<User>> violations = validator.validate(user);
    
    assertThat(violations).isEmpty();
  }

  @Test
  void shouldFailValidationWhenNameIsNull() {
    User user = new User(null, "alice@example.com", 25);
    
    Set<ConstraintViolation<User>> violations = validator.validate(user);
    
    assertThat(violations)
      .hasSize(1)
      .extracting(ConstraintViolation::getMessage)
      .contains("must not be blank");
  }
}

Testing Individual Constraint Annotations

测试单个约束注解

Test @NotNull, @NotBlank, @Email

测试@NotNull、@NotBlank、@Email

java
class UserDtoTest {

  private Validator validator;

  @BeforeEach
  void setUp() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
  }

  @Test
  void shouldFailWhenEmailIsInvalid() {
    UserDto dto = new UserDto("Alice", "invalid-email");
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(dto);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getPropertyPath)
      .extracting(Path::toString)
      .contains("email");
    assertThat(violations)
      .extracting(ConstraintViolation::getMessage)
      .contains("must be a valid email address");
  }

  @Test
  void shouldFailWhenNameIsBlank() {
    UserDto dto = new UserDto("   ", "alice@example.com");
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(dto);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getPropertyPath)
      .extracting(Path::toString)
      .contains("name");
  }

  @Test
  void shouldFailWhenAgeIsNegative() {
    UserDto dto = new UserDto("Alice", "alice@example.com", -5);
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(dto);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getMessage)
      .contains("must be greater than or equal to 0");
  }

  @Test
  void shouldPassWhenAllConstraintsSatisfied() {
    UserDto dto = new UserDto("Alice", "alice@example.com", 25);
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(dto);
    
    assertThat(violations).isEmpty();
  }
}
java
class UserDtoTest {

  private Validator validator;

  @BeforeEach
  void setUp() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
  }

  @Test
  void shouldFailWhenEmailIsInvalid() {
    UserDto dto = new UserDto("Alice", "invalid-email");
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(dto);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getPropertyPath)
      .extracting(Path::toString)
      .contains("email");
    assertThat(violations)
      .extracting(ConstraintViolation::getMessage)
      .contains("must be a valid email address");
  }

  @Test
  void shouldFailWhenNameIsBlank() {
    UserDto dto = new UserDto("   ", "alice@example.com");
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(dto);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getPropertyPath)
      .extracting(Path::toString)
      .contains("name");
  }

  @Test
  void shouldFailWhenAgeIsNegative() {
    UserDto dto = new UserDto("Alice", "alice@example.com", -5);
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(dto);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getMessage)
      .contains("must be greater than or equal to 0");
  }

  @Test
  void shouldPassWhenAllConstraintsSatisfied() {
    UserDto dto = new UserDto("Alice", "alice@example.com", 25);
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(dto);
    
    assertThat(violations).isEmpty();
  }
}

Testing @Min, @Max, @Size Constraints

测试@Min、@Max、@Size约束

java
class ProductDtoTest {

  private Validator validator;

  @BeforeEach
  void setUp() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
  }

  @Test
  void shouldFailWhenPriceIsBelowMinimum() {
    ProductDto product = new ProductDto("Laptop", -100.0);
    
    Set<ConstraintViolation<ProductDto>> violations = validator.validate(product);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getMessage)
      .contains("must be greater than 0");
  }

  @Test
  void shouldFailWhenQuantityExceedsMaximum() {
    ProductDto product = new ProductDto("Laptop", 1000.0, 999999);
    
    Set<ConstraintViolation<ProductDto>> violations = validator.validate(product);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getMessage)
      .contains("must be less than or equal to 10000");
  }

  @Test
  void shouldFailWhenDescriptionTooLong() {
    String longDescription = "x".repeat(1001);
    ProductDto product = new ProductDto("Laptop", 1000.0, longDescription);
    
    Set<ConstraintViolation<ProductDto>> violations = validator.validate(product);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getMessage)
      .contains("size must be between 0 and 1000");
  }
}
java
class ProductDtoTest {

  private Validator validator;

  @BeforeEach
  void setUp() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
  }

  @Test
  void shouldFailWhenPriceIsBelowMinimum() {
    ProductDto product = new ProductDto("Laptop", -100.0);
    
    Set<ConstraintViolation<ProductDto>> violations = validator.validate(product);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getMessage)
      .contains("must be greater than 0");
  }

  @Test
  void shouldFailWhenQuantityExceedsMaximum() {
    ProductDto product = new ProductDto("Laptop", 1000.0, 999999);
    
    Set<ConstraintViolation<ProductDto>> violations = validator.validate(product);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getMessage)
      .contains("must be less than or equal to 10000");
  }

  @Test
  void shouldFailWhenDescriptionTooLong() {
    String longDescription = "x".repeat(1001);
    ProductDto product = new ProductDto("Laptop", 1000.0, longDescription);
    
    Set<ConstraintViolation<ProductDto>> violations = validator.validate(product);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getMessage)
      .contains("size must be between 0 and 1000");
  }
}

Testing Custom Validators

测试自定义验证器

Create and Test Custom Constraint

创建并测试自定义约束

java
// Custom constraint annotation
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface ValidPhoneNumber {
  String message() default "invalid phone number format";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
}

// Custom validator implementation
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
  private static final String PHONE_PATTERN = "^\\d{3}-\\d{3}-\\d{4}$";

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    if (value == null) return true; // null values handled by @NotNull
    return value.matches(PHONE_PATTERN);
  }
}

// Unit test for custom validator
class PhoneNumberValidatorTest {

  private Validator validator;

  @BeforeEach
  void setUp() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
  }

  @Test
  void shouldAcceptValidPhoneNumber() {
    Contact contact = new Contact("Alice", "555-123-4567");
    
    Set<ConstraintViolation<Contact>> violations = validator.validate(contact);
    
    assertThat(violations).isEmpty();
  }

  @Test
  void shouldRejectInvalidPhoneNumberFormat() {
    Contact contact = new Contact("Alice", "5551234567"); // No dashes
    
    Set<ConstraintViolation<Contact>> violations = validator.validate(contact);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getMessage)
      .contains("invalid phone number format");
  }

  @Test
  void shouldRejectPhoneNumberWithLetters() {
    Contact contact = new Contact("Alice", "ABC-DEF-GHIJ");
    
    Set<ConstraintViolation<Contact>> violations = validator.validate(contact);
    
    assertThat(violations).isNotEmpty();
  }

  @Test
  void shouldAllowNullPhoneNumber() {
    Contact contact = new Contact("Alice", null);
    
    Set<ConstraintViolation<Contact>> violations = validator.validate(contact);
    
    assertThat(violations).isEmpty();
  }
}
java
// Custom constraint annotation
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface ValidPhoneNumber {
  String message() default "invalid phone number format";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
}

// Custom validator implementation
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
  private static final String PHONE_PATTERN = "^\\d{3}-\\d{3}-\\d{4}$";

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    if (value == null) return true; // null values handled by @NotNull
    return value.matches(PHONE_PATTERN);
  }
}

// Unit test for custom validator
class PhoneNumberValidatorTest {

  private Validator validator;

  @BeforeEach
  void setUp() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
  }

  @Test
  void shouldAcceptValidPhoneNumber() {
    Contact contact = new Contact("Alice", "555-123-4567");
    
    Set<ConstraintViolation<Contact>> violations = validator.validate(contact);
    
    assertThat(violations).isEmpty();
  }

  @Test
  void shouldRejectInvalidPhoneNumberFormat() {
    Contact contact = new Contact("Alice", "5551234567"); // No dashes
    
    Set<ConstraintViolation<Contact>> violations = validator.validate(contact);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getMessage)
      .contains("invalid phone number format");
  }

  @Test
  void shouldRejectPhoneNumberWithLetters() {
    Contact contact = new Contact("Alice", "ABC-DEF-GHIJ");
    
    Set<ConstraintViolation<Contact>> violations = validator.validate(contact);
    
    assertThat(violations).isNotEmpty();
  }

  @Test
  void shouldAllowNullPhoneNumber() {
    Contact contact = new Contact("Alice", null);
    
    Set<ConstraintViolation<Contact>> violations = validator.validate(contact);
    
    assertThat(violations).isEmpty();
  }
}

Testing Cross-Field Validation

测试跨字段验证

Custom Multi-Field Constraint

自定义多字段约束

java
// Custom constraint for cross-field validation
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchValidator.class)
public @interface PasswordsMatch {
  String message() default "passwords do not match";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
}

// Validator implementation
public class PasswordMatchValidator implements ConstraintValidator<PasswordsMatch, ChangePasswordRequest> {
  @Override
  public boolean isValid(ChangePasswordRequest value, ConstraintValidatorContext context) {
    if (value == null) return true;
    return value.getNewPassword().equals(value.getConfirmPassword());
  }
}

// Unit test
class PasswordValidationTest {

  private Validator validator;

  @BeforeEach
  void setUp() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
  }

  @Test
  void shouldPassWhenPasswordsMatch() {
    ChangePasswordRequest request = new ChangePasswordRequest("oldPass", "newPass123", "newPass123");
    
    Set<ConstraintViolation<ChangePasswordRequest>> violations = validator.validate(request);
    
    assertThat(violations).isEmpty();
  }

  @Test
  void shouldFailWhenPasswordsDoNotMatch() {
    ChangePasswordRequest request = new ChangePasswordRequest("oldPass", "newPass123", "differentPass");
    
    Set<ConstraintViolation<ChangePasswordRequest>> violations = validator.validate(request);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getMessage)
      .contains("passwords do not match");
  }
}
java
// Custom constraint for cross-field validation
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchValidator.class)
public @interface PasswordsMatch {
  String message() default "passwords do not match";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
}

// Validator implementation
public class PasswordMatchValidator implements ConstraintValidator<PasswordsMatch, ChangePasswordRequest> {
  @Override
  public boolean isValid(ChangePasswordRequest value, ConstraintValidatorContext context) {
    if (value == null) return true;
    return value.getNewPassword().equals(value.getConfirmPassword());
  }
}

// Unit test
class PasswordValidationTest {

  private Validator validator;

  @BeforeEach
  void setUp() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
  }

  @Test
  void shouldPassWhenPasswordsMatch() {
    ChangePasswordRequest request = new ChangePasswordRequest("oldPass", "newPass123", "newPass123");
    
    Set<ConstraintViolation<ChangePasswordRequest>> violations = validator.validate(request);
    
    assertThat(violations).isEmpty();
  }

  @Test
  void shouldFailWhenPasswordsDoNotMatch() {
    ChangePasswordRequest request = new ChangePasswordRequest("oldPass", "newPass123", "differentPass");
    
    Set<ConstraintViolation<ChangePasswordRequest>> violations = validator.validate(request);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getMessage)
      .contains("passwords do not match");
  }
}

Testing Validation Groups

测试验证组

Conditional Validation

条件验证

java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public interface CreateValidation {}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public interface UpdateValidation {}

class UserDto {
  @NotNull(groups = {CreateValidation.class})
  private String name;

  @Min(value = 1, groups = {CreateValidation.class, UpdateValidation.class})
  private int age;
}

class ValidationGroupsTest {

  private Validator validator;

  @BeforeEach
  void setUp() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
  }

  @Test
  void shouldRequireNameOnlyDuringCreation() {
    UserDto user = new UserDto(null, 25);
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(user, CreateValidation.class);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getPropertyPath)
      .extracting(Path::toString)
      .contains("name");
  }

  @Test
  void shouldAllowNullNameDuringUpdate() {
    UserDto user = new UserDto(null, 25);
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(user, UpdateValidation.class);
    
    assertThat(violations).isEmpty();
  }
}
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public interface CreateValidation {}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public interface UpdateValidation {}

class UserDto {
  @NotNull(groups = {CreateValidation.class})
  private String name;

  @Min(value = 1, groups = {CreateValidation.class, UpdateValidation.class})
  private int age;
}

class ValidationGroupsTest {

  private Validator validator;

  @BeforeEach
  void setUp() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
  }

  @Test
  void shouldRequireNameOnlyDuringCreation() {
    UserDto user = new UserDto(null, 25);
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(user, CreateValidation.class);
    
    assertThat(violations)
      .extracting(ConstraintViolation::getPropertyPath)
      .extracting(Path::toString)
      .contains("name");
  }

  @Test
  void shouldAllowNullNameDuringUpdate() {
    UserDto user = new UserDto(null, 25);
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(user, UpdateValidation.class);
    
    assertThat(violations).isEmpty();
  }
}

Testing Parameterized Validation Scenarios

测试参数化验证场景

java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class EmailValidationTest {

  private Validator validator;

  @BeforeEach
  void setUp() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
  }

  @ParameterizedTest
  @ValueSource(strings = {
    "user@example.com",
    "john.doe+tag@example.co.uk",
    "admin123@subdomain.example.com"
  })
  void shouldAcceptValidEmails(String email) {
    UserDto user = new UserDto("Alice", email);
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(user);
    
    assertThat(violations).isEmpty();
  }

  @ParameterizedTest
  @ValueSource(strings = {
    "invalid-email",
    "user@",
    "@example.com",
    "user name@example.com"
  })
  void shouldRejectInvalidEmails(String email) {
    UserDto user = new UserDto("Alice", email);
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(user);
    
    assertThat(violations).isNotEmpty();
  }
}
java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class EmailValidationTest {

  private Validator validator;

  @BeforeEach
  void setUp() {
    validator = Validation.buildDefaultValidatorFactory().getValidator();
  }

  @ParameterizedTest
  @ValueSource(strings = {
    "user@example.com",
    "john.doe+tag@example.co.uk",
    "admin123@subdomain.example.com"
  })
  void shouldAcceptValidEmails(String email) {
    UserDto user = new UserDto("Alice", email);
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(user);
    
    assertThat(violations).isEmpty();
  }

  @ParameterizedTest
  @ValueSource(strings = {
    "invalid-email",
    "user@",
    "@example.com",
    "user name@example.com"
  })
  void shouldRejectInvalidEmails(String email) {
    UserDto user = new UserDto("Alice", email);
    
    Set<ConstraintViolation<UserDto>> violations = validator.validate(user);
    
    assertThat(violations).isNotEmpty();
  }
}

Best Practices

最佳实践

  • Validate at unit test level before testing service/controller layers
  • Test both valid and invalid cases for every constraint
  • Use custom validators for business-specific validation rules
  • Test error messages to ensure they're user-friendly
  • Test edge cases: null, empty string, whitespace-only strings
  • Use validation groups for conditional validation rules
  • Keep validator logic simple - complex validation belongs in service tests
  • 在单元测试层验证,再测试服务/控制器层
  • 为每个约束测试有效和无效场景
  • 使用自定义验证器处理业务特定的验证规则
  • 测试错误消息,确保对用户友好
  • 测试边缘情况:null、空字符串、仅含空白字符的字符串
  • 使用验证组实现条件验证规则
  • 保持验证器逻辑简洁 - 复杂验证应放在服务测试中

Common Pitfalls

常见误区

  • Forgetting to test null values
  • Not extracting violation details (message, property, constraint type)
  • Testing validation at service/controller level instead of unit tests
  • Creating overly complex custom validators
  • Not documenting constraint purposes in error messages
  • 忘记测试null值
  • 未提取违例详情(消息、属性、约束类型)
  • 在服务/控制器层测试验证,而非单元测试
  • 创建过于复杂的自定义验证器
  • 未在错误消息中说明约束的用途

Troubleshooting

问题排查

ValidatorFactory not found: Ensure
jakarta.validation-api
and
hibernate-validator
are on classpath.
Custom validator not invoked: Verify
@Constraint(validatedBy = YourValidator.class)
is correctly specified.
Null handling confusion: By default,
@NotNull
checks null, other constraints ignore null (use
@NotNull
with others for mandatory fields).
ValidatorFactory未找到:确保
jakarta.validation-api
hibernate-validator
已添加到类路径。
自定义验证器未被调用:确认已正确指定
@Constraint(validatedBy = YourValidator.class)
Null处理混淆:默认情况下,
@NotNull
检查null值,其他约束会忽略null(如需强制字段必填,需结合
@NotNull
与其他约束)。

References

参考资料