Skip to content

Validating Beans

Jin Kwon edited this page Nov 13, 2026 · 1 revision

Validating Beans

assertThatBean(actual) returns an AbstractBeanAssert<?, ACTUAL>. Each of its methods calls the validator once, with the groups configured at that moment, and asserts on the resulting set of violations. The assertion keeps no result between calls, so one assertion object can be reused, reconfigured, and asserted on again.

AbstractBeanAssert extends AbstractPropertyAssert, so a bean can also be checked as a value of another bean's property with isValidFor / isNotValidFor — see Validating Values. Method parameter and return-value validation also hang off the bean assertion — see Executable Validation.

Methods

Method Jakarta Validation call Passes when
isValid() Validator.validate(actual, groups) the set is empty
isValid(Consumer) same the set is empty
isNotValid() same the set is not empty
isNotValid(Consumer) same the set is not empty
hasValidProperty(String) Validator.validateProperty(actual, name, groups) the set is empty
hasValidProperty(String, Consumer) same the set is empty
doesNotHaveValidProperty(String) same the set is not empty

Every one of them first asserts that actual is not null; a null bean fails as an AssertionError. A null property name or consumer is a NullPointerException.

assertThatBean(new User("Jane", 28)).isValid();
assertThatBean(new User(null, 0)).isNotValid();

assertThatBean(new User("Jane", 28)).hasValidProperty("name");     // passes
assertThatBean(new User(null, 0)).hasValidProperty("age");         // passes: only "age" is validated
assertThatBean(new User("John", -1)).hasValidProperty("age");      // fails
assertThatBean(new User("John", -1)).doesNotHaveValidProperty("age"); // passes

@Valid is not followed by property validation

hasValidProperty and doesNotHaveValidProperty rely on Validator.validateProperty, which by specification does not honour @Valid: the constraints declared on the property itself are checked, but the object it refers to is not cascaded into. Use isValid() on the owning bean when the cascade matters. The same caveat applies to Validating Values.

Groups

targetingGroups(Class<?>...) replaces the groups of the assertion. null or no argument clears them, and an assertion with no groups validates the Default group. A null element is an IllegalArgumentException.

The groups stay configured on the assertion object until the next call:

final var assertion = assertThatBean(new User("name", 30));   // a junior: 0..59

assertion.targetingGroups(Junior.class);
assertion.isValid();                       // passes: @Max(value = 59, groups = Junior.class)

assertion.targetingGroups(Senior.class);
assertion.isNotValid();                    // passes: @Min(value = 60, groups = Senior.class)

The groups also appear in the failure description, as groups: [interface …Junior].

targetingGroups, usingValidator and usingValidatorFactory are declared on AbstractValidationAssert and are final; see Validator Lifecycle for the latter two.

Consumers

The overloads that take a Consumer<? super Set<ConstraintViolation<ACTUAL>>> pass the set of violations to it, so a test can inspect why:

assertThatBean(new User(null, 0)).isNotValid(violations ->
        assertThatConstraintViolations(violations)
                .singleElement()
                .hasMessageTemplate("{jakarta.validation.constraints.NotBlank.message}"));

When the consumer runs, and what it receives, currently depends on the method. Whether it should be an observer that always sees the validation output or a hook that runs only after the assertion passes, and whether it should always receive an unmodifiable set, is undecided and tracked in #9. Do not build on the differences below; they describe the working tree, not a contract.

Overload Consumer runs On a failing assertion Set passed
isValid(Consumer) before the emptiness check the consumer has already seen the violations unmodifiable view
isNotValid(Consumer) after the check passes the consumer never runs the provider's set
hasValidProperty(String, Consumer) after the check passes the consumer never runs the provider's set

isValidFor(…, Consumer), inherited from AbstractPropertyAssert, behaves like isValid(Consumer); the executable-validation overloads behave like isNotValid(Consumer). doesNotHaveValidProperty has no consumer overload (#12).

Anything the consumer throws propagates unchanged; an AssertionError from an assertion made inside it fails the test as usual.

To capture violations from an assertion that is expected to fail, the "before" overloads are the ones that see them today — this is how User_ConstraintViolationAssert_Test collects them:

final var violations = new HashSet<ConstraintViolation<User>>();
assertThatThrownBy(() -> assertThatProperty("  ").isValidFor(User.class, "name", violations::addAll))
        .isInstanceOf(AssertionError.class);
assertThat(violations).hasSize(1);

Clone this wiki locally