Skip to content

Getting Started

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

Getting Started

Requirements

Java (to consume the artifact) 17+ (main classes are compiled with --release 17)
Java (to build this repository) 25 (test sources compile with --release 25)
Jakarta Validation 3.0 or 3.1
AssertJ 3.x from 3.27.7; 4.0.0-M1 tested as a milestone
Provider any Jakarta Validation implementation on the test classpath; only Hibernate Validator is tested

Dependencies

The artifact is meant for test scope. Its own two dependencies — jakarta.validation-api and assertj-core — are provided, so nothing comes with it transitively: your build declares the API, AssertJ, and a provider.

<!-- this library -->
<dependency>
  <groupId>io.github.jinahya</groupId>
  <artifactId>assertj-validation</artifactId>
  <version><!-- released version --></version>
  <scope>test</scope>
</dependency>

<!-- the API; the application usually has it already -->
<dependency>
  <groupId>jakarta.validation</groupId>
  <artifactId>jakarta.validation-api</artifactId>
  <version>3.1.1</version>
  <scope>provided</scope>
</dependency>

<!-- AssertJ -->
<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <version>3.27.7</version>
  <scope>test</scope>
</dependency>

<!-- a provider, and the expression language it interpolates messages with -->
<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>

These are the versions this repository's default profile (jakartaee-11) builds with. For Jakarta Validation 3.0 (Jakarta EE 10) the build uses jakarta.validation-api 3.0.2, Hibernate Validator 8.0.5.Final and Expressly 5.0.0. Hibernate Validator and Expressly move together: 9 ↔ 6, 8 ↔ 5. See Build and Profiles.

The examples below describe the working tree (0.6.2-SNAPSHOT). assertThatConstraintViolationException is not in 0.6.1. See Published artifact.

A bean to validate

The examples on this wiki are adapted from the repository's tests (src/test/java/…/example/user and …/example/service). The central fixture is a User:

class User {

    @NotBlank
    private final String name;

    @Max(127)
    @Min(value = 60, groups = Senior.class)
    @Max(value = 59, groups = Junior.class)
    @PositiveOrZero
    private final int age;

    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

interface Junior {}
interface Senior {}

First assertions

import static com.github.jinahya.assertj.validation.ValidationAssertions.*;

assertThatBean(new User("Jane", 28)).isValid();           // passes
assertThatBean(new User(null, 0)).isNotValid();           // passes: name is blank
assertThatBean(new User("John", -1)).isNotValid();        // passes: age is negative

assertThatBean(new User("Jane", 28)).hasValidProperty("name");
assertThatBean(new User(null, 0)).doesNotHaveValidProperty("name");

A value can be checked against a property without building a bean:

assertThatProperty("Jane").isValidFor(User.class, "name");   // passes
assertThatProperty("   ").isNotValidFor(User.class, "name"); // passes
assertThatProperty(300).isNotValidFor(User.class, "age");    // passes: @Max(127)

Groups are selected per assertion:

assertThatProperty(59)
        .isValidFor(User.class, "age")                                  // Default
        .targetingGroups(Junior.class).isValidFor(User.class, "age")
        .targetingGroups(Senior.class).isNotValidFor(User.class, "age");

What a failure says

A failing validity assertion lists the bean, the groups, and every violation:

The set of constraint violations resulted while validating
	actual: User@3c1{name=null,age=0}
targeting
	groups: [interface 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}

What was validated comes first, then the expectation and one -> entry per violation. The fragments pinned by User_FailureMessages_Test are the description text, the groups, the count, and the propertyPath / rootBeanClass / messageTemplate lines. The message line is interpolated and depends on the locale.

The failure goes through the assertion itself, so what you configure on it is honoured. A description set with as(…) prefixes the text, in AssertJ's brackets, and a message set with withFailMessage(…) replaces it, the lazy Supplier form being evaluated only on failure:

assertThatBean(user).as("registration case 42").isValid();               // [registration case 42] The set of …
assertThatBean(user).withFailMessage("registration rejected").isValid(); // registration rejected

Inspecting the violations

The isValid(Consumer) / isNotValid(Consumer) overloads hand the violation set to a consumer, which can use the inspection assertions:

assertThatBean(new User(null, -1)).isNotValid(violations ->
        assertThatConstraintViolations(violations)
                .hasSize(2)
                .filteredOn(v -> v.getPropertyPath().toString().equals("name"))
                .first()
                .hasMessageTemplate("{jakarta.validation.constraints.NotBlank.message}"));

The consumer runs at a different point in different methods today; see Consumers. See Inspecting Violations for the violation assertions.

Where the validator comes from

With no configuration, each assertion builds a ValidatorFactory and closes it before returning. A suite that wants to avoid that cost passes its own:

try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
    assertThatBean(user).usingValidatorFactory(factory).isValid();
    assertThatBean(other).usingValidatorFactory(factory).isNotValid();
}   // the caller closes it, here

See Validator Lifecycle.

Clone this wiki locally