Skip to content

Paths and Nodes

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

Paths and Nodes

A violation's getPropertyPath() is a jakarta.validation.Path: a sequence of Path.Nodes, each of an ElementKind. assertThatPath(Path) returns an AbstractPathAssert<?>; it is also reached from a violation with extractingPropertyPath().

The path assertion

AbstractPathAssert extends AbstractAssert<SELF, Path> and implements AssertJ's EnumerableAssert. It is deliberately not an AbstractIterableAssert: that would require filteredOn to return a Path, and a filtered subset of nodes is not a path. So the path assertion has the size vocabulary, and nodes() has the rest.

On the path
size isEmpty(), isNotEmpty(), isNullOrEmpty(), hasSize(int), hasSizeGreaterThan(int), hasSizeGreaterThanOrEqualTo(int), hasSizeLessThan(int), hasSizeLessThanOrEqualTo(int), hasSizeBetween(int, int), hasSameSizeAs(Iterable), hasSameSizeAs(Object array)
nodes as a list nodes() — a ListAssert<Path.Node>: filteredOn, contains, allSatisfy, extracting, …
node by position extractingNode(int) and the typed extracting…Node(int) below
not supported usingElementComparator(…), usingDefaultElementComparator() throw UnsupportedOperationException; configure a comparator on nodes() instead
assertThatPath(path).hasSize(2);
assertThatPath(path).nodes().filteredOn(Path.Node::isInIterable).isEmpty();

Typed node navigation

For a node of a known kind at a known index, use the typed navigation; it returns the node assertion for that kind.

Kind (ElementKind) Node type Extract Consumer form Standalone entry point
any Path.Node extractingNode(int) hasNodeSatisfying(int, Consumer) assertThatNode
BEAN Path.BeanNode extractingBeanNode(int) — assertThatBeanNode
PROPERTY Path.PropertyNode extractingPropertyNode(int) hasPropertyNodeSatisfying(int, Consumer) assertThatPropertyNode
METHOD Path.MethodNode extractingMethodNode(int) hasMethodNodeSatisfying(int, Consumer) assertThatMethodNode
CONSTRUCTOR Path.ConstructorNode extractingConstructorNode(int) hasConstructorNodeSatisfying(int, Consumer) assertThatConstructorNode
PARAMETER Path.ParameterNode extractingParameterNode(int) hasParameterNodeSatisfying(int, Consumer) assertThatParameterNode
CROSS_PARAMETER Path.CrossParameterNode extractingCrossParameterNode(int) hasCrossParameterNodeSatisfying(int, Consumer) assertThatCrossParameterNode
RETURN_VALUE Path.ReturnValueNode extractingReturnValueNode(int) hasReturnValueNodeSatisfying(int, Consumer) assertThatReturnValueNode
CONTAINER_ELEMENT Path.ContainerElementNode extractingContainerElementNode(int) hasContainerElementNodeSatisfying(int, Consumer) assertThatContainerElementNode

Each extracting…Node(int) also has an overload taking an AssertFactory, and the general form is extractingNode(int index, Class<N> nodeType, AssertFactory<? super N, ? extends A> factory) with hasNodeSatisfying(int, Class<N>, AssertFactory, Consumer).

The path of a method-parameter violation has two nodes — the method, then the parameter:

final Method greet = Greeter.class.getMethod("greet", String.class, int.class);
final Path path = validator.forExecutables()
        .validateParameters(new Greeter("Jane"), greet, new Object[]{"  ", 1})
        .iterator().next()
        .getPropertyPath();                                  // greet.arg0

assertThatPath(path)
        .hasSize(2)
        .hasMethodNodeSatisfying(0, n -> n.hasName("greet"))
        .hasNodeSatisfying(1, n -> n.hasKind(ElementKind.PARAMETER));

assertThatPath(path)
        .extractingMethodNode(0)
        .hasParameterTypes(String.class, int.class);

assertThatPath(path)
        .extractingParameterNode(1)
        .hasParameterIndex(0);

What each node assertion offers

Every node assertion has the properties common to Path.Node:

Property Extract Check
getName() extractingName() hasName(String), doesNotHaveName(), hasNameSatisfying(Consumer)
getKind() extractingKind() hasKind(ElementKind), hasKindSatisfying(Consumer)
getIndex() extractingIndex() hasIndex(Integer), doesNotHaveIndex(), hasIndexSatisfying(Consumer)
getKey() extractingKey() hasKey(Object), doesNotHaveKey(), hasKeySatisfying(Consumer)
isInIterable() extractingInIterable() isInIterable(), isNotInIterable(), hasInIterableSatisfying(Consumer)

and, by kind, the properties of its node type, through the mixin interfaces nested in AbstractPathAssert:

Mixin Methods Implemented by
HasContainerClass extractingContainerClass(), hasContainerClass(Class), doesNotHaveContainerClass(), hasContainerClassSatisfying(…) bean, property, container-element nodes
HasTypeArgumentIndex extractingTypeArgumentIndex(), hasTypeArgumentIndex(Integer), doesNotHaveTypeArgumentIndex(), hasTypeArgumentIndexSatisfying(…) bean, property, container-element nodes
HasParameterTypes extractingParameterTypes(), hasParameterTypes(Class...), hasParameterTypesSatisfying(…) method, constructor nodes
HasParameterIndex extractingParameterIndex(), hasParameterIndex(Integer), doesNotHaveParameterIndex(), hasParameterIndexSatisfying(…) parameter nodes

Cross-parameter and return-value nodes have the common properties only. Each extracting… and …Satisfying also has an overload taking an AssertFactory.

A list element is a property node followed by a container-element node. For the fixture Bagged, whose List<@NotBlank String> tags holds a blank string:

assertThatPath(tagsPath).extractingPropertyNode(0)
        .hasName("tags")
        .doesNotHaveContainerClass()
        .doesNotHaveTypeArgumentIndex()
        .isNotInIterable();

assertThatPath(tagsPath).extractingContainerElementNode(1)
        .hasKind(ElementKind.CONTAINER_ELEMENT)
        .hasContainerClass(List.class)
        .hasTypeArgumentIndex(0)
        .hasIndex(0)
        .isInIterable();

A class-level constraint yields a single BEAN node, the only kind whose name is null:

assertThatPath(beanPath).extractingBeanNode(0)
        .hasKind(ElementKind.BEAN)
        .doesNotHaveName()
        .doesNotHaveIndex()
        .doesNotHaveKey()
        .isNotInIterable();

Kind checking: why getKind(), not instanceof

Narrowing a node to a subtype is checked against Path.Node.getKind(), never with instanceof. Hibernate Validator's node implementation implements every Path.Node subtype at once, so Path.PropertyNode.class.isInstance(node) is true even for a METHOD node; PathAssert_NodeKind_Test asserts that premise. And Path.Node.as(Class) is specified to throw a ClassCastException for the wrong kind, with the advice to check getKind() first.

So extractingPropertyNode(0) on a method node fails as an AssertionError naming both kinds, rather than succeeding by instanceof and failing later inside the provider:

Expecting the node at index 0
  <…the node's toString()…>
to be of kind
  <PROPERTY>
so that it can be narrowed to <PropertyNode>, but its kind is
  <METHOD>

The node factories in InstanceOf Factories (PROPERTY_NODE, METHOD_NODE, …) apply the same check, since the instanceof test that asInstanceOf performs first cannot discriminate. The standalone assertThat…Node entry points take an already-typed node and do not check; narrow with node.as(…) only after checking the kind.

Index contract

For every positional navigation — extractingNode, the typed extracting…Node, the has…NodeSatisfying forms, and extractingParameterDescriptor(int) on Metadata:

Index Result
negative IllegalArgumentException — a programming error in the test
at or beyond the number of nodes (or parameters) AssertionError: Expecting a node at index 2 of <greet.arg0> but it has 2 node(s)
a node of another kind, for a typed navigation AssertionError, as above

Both failures are raised through the path assertion, so what was configured on it is kept: assertThatPath(path).as("case 42").extractingNode(2) fails with [case 42] in the message, and a withFailMessage(…) replaces the text. The navigated node assertion — and the list from nodes() — inherits the description and the overriding message too.

In 0.6.1 a missing index surfaced as NoSuchElementException (paths) or IndexOutOfBoundsException (parameter descriptors).

Clone this wiki locally