Skip to content

Executable Validation

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

Executable Validation

Method and constructor constraints are checked through Validator.forExecutables(), the ExecutableValidator. Nothing is invoked: the test supplies the argument values or the return value, and the assertion validates them.

Method validation needs the instance the method belongs to, so it hangs off the bean assertion. Constructor validation has no instance yet, so it has an assertion of its own whose actual is the Constructor.

The fixture used below, from example/service:

public class Greeter {

    @NotNull
    @Named                                      // a return-value constraint: the created Greeter has a non-blank name
    public Greeter(@NotBlank final String name) {
        this.name = name;
    }

    @NotBlank
    public String greet(@NotBlank final String whom, @Positive final int times) {
        return (name + " greets " + whom + ' ').repeat(Math.max(times, 1));
    }

    public String getName() {
        return name;
    }

    @NotBlank
    private final String name;
}

Methods: assertThatBean(instance)

Method ExecutableValidator call Passes when
hasValidParameters(Method, Object...) validateParameters(actual, method, values, groups) no violation
hasValidParameters(Method, Object[], Consumer) same no violation
doesNotHaveValidParameters(Method, Object...) same at least one violation
hasValidReturnValue(Method, Object) validateReturnValue(actual, method, value, groups) no violation
hasValidReturnValue(Method, Object, Consumer) same no violation
doesNotHaveValidReturnValue(Method, Object) same at least one violation
final Method greet = Greeter.class.getMethod("greet", String.class, int.class);
final var greeter = new Greeter("Jane");

assertThatBean(greeter).hasValidParameters(greet, "world", 1);
assertThatBean(greeter).doesNotHaveValidParameters(greet, "  ", 0);

assertThatBean(greeter).hasValidReturnValue(greet, "Jane greets world");
assertThatBean(greeter).doesNotHaveValidReturnValue(greet, "   ");

With a consumer, the argument values are passed as an explicit Object[], since the consumer follows them:

assertThatBean(greeter).hasValidParameters(greet, new Object[]{"world", 1},
                                           violations -> assertThat(violations).isEmpty());

The bean must not be null (an AssertionError), and neither may the Method (a NullPointerException).

Constructors: assertThatConstructor(constructor)

assertThatConstructor(Constructor<? extends T>) returns an AbstractConstructorAssert<?, T>:

Method ExecutableValidator call Passes when
hasValidParameters(Object...) validateConstructorParameters(actual, values, groups) no violation
hasValidParameters(Object[], Consumer) same no violation
doesNotHaveValidParameters(Object...) same at least one violation
hasValidReturnValue(T createdObject) validateConstructorReturnValue(actual, createdObject, groups) no violation
hasValidReturnValue(T, Consumer) same no violation
doesNotHaveValidReturnValue(T) same at least one violation
final Constructor<Greeter> constructor = Greeter.class.getConstructor(String.class);

assertThatConstructor(constructor).hasValidParameters("Jane");
assertThatConstructor(constructor).doesNotHaveValidParameters(new Object[]{"  "});

assertThatConstructor(constructor).hasValidReturnValue(new Greeter("Jane"));

final var blank = new Greeter("   ");   // parameter constraints do not run at construction time
assertThatConstructor(constructor).doesNotHaveValidReturnValue(blank);

Two things about constructor return values follow from the specification rather than from this library, and the tests pin both:

  • A null created object is not a constraint violation. The provider rejects it with an IllegalArgumentException before any constraint runs, and the assertion lets that propagate; it does not turn it into an assertion failure. A @NotNull on a constructor can therefore never fail — which is why the fixture has the custom @Named constraint.

  • Standard constraints on the created object are reached through @Valid. A constructor carrying @NotNull @Valid cascades into the new instance, so a @NotBlank field of it is checked:

    public static class Account {
        @NotNull @Valid
        public Account(final String id) { this.id = id; }
    
        @NotBlank private final String id;
    }
    
    assertThatConstructor(Account.class.getConstructor(String.class))
            .doesNotHaveValidReturnValue(new Account(" "));

Groups, validator, failure text

Both assertions extend AbstractValidationAssert, so targetingGroups, usingValidator and usingValidatorFactory apply as on Validating Beans and Validator Lifecycle.

A failure names what was validated, in the same shape for methods and constructors:

The set of constraint violations resulted while validating
	actual: …Greeter@…
for the
	parameters of
	method: public java.lang.String …Greeter.greet(java.lang.String,int)
targeting
	groups: [interface jakarta.validation.groups.Default]
expected to be empty but contains 1 element(s)
-> 	…
The set of constraint violations resulted while validating
	return value of
	constructor: public …Greeter(java.lang.String)
targeting
	groups: [interface jakarta.validation.groups.Default]
expected to be not empty but empty

Consumers

The consumer overloads run the consumer only after the assertion has passed, and pass the provider's set as is. That differs from isValid(Consumer) and is open in #9. The negative methods (doesNotHaveValidParameters, doesNotHaveValidReturnValue) have no consumer overload, so a test that expects an invalid argument cannot see which constraint failed through this API today (#12); call the ExecutableValidator directly and use Inspecting Violations on the result.

Clone this wiki locally