An AssertJ extension for Jakarta Validation.
<dependency>
<groupId>io.github.jinahya</groupId>
<artifactId>assertj-validation</artifactId>
<version>${version.io.github.jinahya.assertj-validation}</version>
<scope>test</scope>
</dependency>The two APIs it extends are provided: bring your own, along with a validation provider for default validation. For Jakarta EE 11, e.g.,
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.1.1</version>
<scope>provided</scope>
</dependency>
<!-- the provider, and the expression language it interpolates messages with; for Jakarta EE 10, use 8.0.x and 5.0.x -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>9.1.4.Final</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.expressly</groupId>
<artifactId>expressly</artifactId>
<version>6.0.0</version>
<scope>test</scope>
</dependency>A test which only ever hands the assertions a validator of its own (usingValidator, usingValidatorFactory) still
needs whatever produced that validator; no provider is reached for otherwise.
Both APIs this module sits between are provided-scoped, so a consumer brings its own:
| Java | 17 or later at runtime; JDK 25 to build and run the test suite |
| Jakarta Validation | 3.0 and 3.1 |
| AssertJ | 3.x from 3.27.7; 4.0.0-M1 is tested as a milestone, and later 4.x releases are verified as they come |
What is tested, on every push:
- the whole suite on JDK 25, for each Jakarta generation, with AssertJ 3.27.7 and with 4.0.0-M1;
- a consumer on Java 17, against the jar as packaged (built on AssertJ 3), for each AssertJ line and each Jakarta
generation:
.ci/java17.
The build runs against one generation at a time, selected by a profile. Each pins the platform BOM — which in
turn pins jakarta.validation-api — along with the Hibernate Validator and Expressly releases aligned with it.
| profile | jakarta.validation-api |
Hibernate Validator | Expressly |
|---|---|---|---|
jakartaee-11 (default) |
3.1.1 | 9 | 6 |
jakartaee-10 |
3.0.2 | 8 | 5 |
./mvnw -P jakartaee-10 testThe build compiles against AssertJ 3, which every consumer can run, and targets AssertJ 4: the assertj-4 profile
runs the same build against it. Combine it with a generation.
./mvnw -P jakartaee-11,assertj-4 testOnly Hibernate Validator and Expressly are test-scoped: nothing under src/main references an implementation,
which is the point of extending the API rather than the engine.
All entry points are static methods of ValidationAssertions. As in AssertJ, ValidationBDDAssertions offers each as
then… (thenBean(user).isValid()), and a test class may implement WithValidationAssertions instead of importing
them.
import static com.github.jinahya.assertj.validation.ValidationAssertions.*;
// beans, and their properties
assertThatBean(user).isValid();
assertThatBean(user).targetingGroups(Senior.class).isNotValid();
assertThatBean(user).hasValidProperty("name");
// a value, against the constraints of a property, without a bean
assertThatProperty("").isNotValidFor(User.class, "name");
// executables
assertThatBean(greeter).hasValidParameters(method, "world", 1);
assertThatConstructor(constructor).doesNotHaveValidReturnValue(createdObject);
// what validation produced
assertThatConstraintViolations(violations)
.filteredOn(v -> v.getPropertyPath().toString().equals("name"))
.first()
.hasMessageTemplate("{jakarta.validation.constraints.NotBlank.message}");
assertThatConstraintViolationException(thrown).hasViolationOn("name");
assertThatPath(violation.getPropertyPath()).extractingPropertyNode(0).hasName("name");
// metadata
assertThatBeanDescriptor(validator.getConstraintsForClass(User.class))
.extractingConstraintsForProperty("name")
.hasConstraints();A failure lists every violation, and honours as(..) and withFailMessage(..):
[registration case 42]
The set of constraint violations resulted while validating
actual: User@3c1{name=null,age=0}
targeting
groups: [jakarta.validation.groups.Default]
expected to be empty but contains 1 element(s)
-> message : must not be blank
propertyPath : name
rootBeanClass : class User
messageTemplate: {jakarta.validation.constraints.NotBlank.message}
With nothing configured, each assertion builds a ValidatorFactory and closes it before returning. A suite which
minds the cost hands in its own, which the assertions use but never close:
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
assertThatBean(user).usingValidatorFactory(factory).isValid();
assertThatBean(user).usingValidator(factory.getValidator()).isValid();
}Each assertion method can be intercepted by SoftAssertions.proxy(..), so a failure is collected rather than thrown:
SoftAssertions softly = new SoftAssertions();
softly.proxy(DefaultBeanAssert.class, Object.class, user).isValid();
softly.proxy(DefaultBeanAssert.class, Object.class, user).hasValidProperty("age");
softly.assertAll(); // reports bothOn AssertJ 3.x and 4.0.0-M1, a chain which navigates into a nested assertion, e.g. extractingPropertyPath() or
extractingNode(0), is not a soft chain: released AssertJ re-proxies only the results of its own navigation methods.
Wrap such a chain in check(..), which collects its first failure and lets the next check run:
softly.check(() -> assertThatConstraintViolation(violation).extractingPropertyPath().hasSize(1));The jar is a named module, com.github.jinahya.assertj.validation, which requires org.assertj.core and
jakarta.validation transitively. On the module path everything works as on the class path, except
SoftAssertions.proxy(..): on AssertJ 3.x and 4.0.0-M1, AssertJ generates the proxy into this module's package. The
module opens that package to org.assertj.core, but AssertJ must also read this module, and on 3.27.7 the proxy needs
an AssertJ package that AssertJ does not export:
--add-reads org.assertj.core=com.github.jinahya.assertj.validation
--add-exports org.assertj.core/org.assertj.core.api.recursive.assertion=com.github.jinahya.assertj.validation
.ci/jpms runs a module-path consumer on every push.
More in the wiki and the package-info.