JsonPath: nested `properties` over a list of objects returns null / leaks Groovy meta-data because the list spread result is not the proxy list (Groovy 5 regression)

Author: HDPark95Created Jul 22, 2026Updated Jul 22, 2026

Description

For a JSON list of objects, list.properties is treated as a Groovy-4-style GPath spread and returns list.collect { it['properties'] }. This is restored on Groovy 5 by Groovy5JsonSlurperWorkarounds.ProxyArray (issue #552, PR #1844), and a single-level spread works today. Nested properties over a map also works and has a test (parses_immediate_and_nested_properties_in_map_structure_not_wrapped_in_list, properties.properties.height).

The natural combination of the two, a nested properties over a list, silently breaks. The spread result is a plain immutable List (from Stream.toList()) rather than the ProxyArray proxy, so the Groovy 5 workaround does not propagate to it. A second properties hop then resolves to Groovy's meta-property instead of the JSON field:

  • list.properties.properties.x returns null, and
  • list.properties.properties returns the list's Groovy meta-data, {class=class java.util.ImmutableCollections$ListN, empty=false}.

That {class, empty} map is exactly the symptom reported (and long since fixed for the single-level case) in issue #385. It resurfaces here for the nested-over-list case.

rest-assured version

Regression introduced in 6.0.0 (json-path Java migration, PR #1844). Reproduced on master HEAD eeed4998 (6.0.2-SNAPSHOT), Groovy 5.0.3, JDK 17. The pre-migration 5.5.x line used the old GroovyShell path and is not affected. This is independent of #1875: that fix (PR #1878) changes only the spread's element gate, not the type of the spread result, so the nested case stays broken after it.

Steps to reproduce

java
import io.restassured.path.json.JsonPath;

public class Repro {
    public static void main(String[] args) {
        // Works: nested "properties" over a MAP (existing test)
        String map = "{\"properties\":{\"properties\":{\"height\":5}}}";
        System.out.println("map   properties.properties.height       = "
                + JsonPath.from(map).getInt("properties.properties.height"));

        // Works: single-level "properties" spread over a LIST
        String single = "{\"items\":[{\"properties\":{\"gridId\":6}},{\"properties\":{\"gridId\":7}}]}";
        System.out.println("list  items.properties.gridId            = "
                + JsonPath.from(single).getList("items.properties.gridId"));

        // BUG: nested "properties" over a LIST
        String nested = "{\"items\":[{\"properties\":{\"properties\":{\"gridId\":6}}},"
                + "{\"properties\":{\"properties\":{\"gridId\":7}}}]}";
        System.out.println("list  items.properties.properties.gridId = "
                + JsonPath.from(nested).get("items.properties.properties.gridId"));
        System.out.println("list  items.properties.properties        = "
                + JsonPath.from(nested).get("items.properties.properties"));
        System.out.println("list  items.properties (one hop)         = "
                + JsonPath.from(nested).get("items.properties"));
    }
}

Output:

map   properties.properties.height       = 5
list  items.properties.gridId            = [6, 7]
list  items.properties.properties.gridId = null                                          <-- BUG (expected [6, 7])
list  items.properties.properties        = {class=class java.util.ImmutableCollections$ListN, empty=false}   <-- BUG (Groovy meta-data leaked)
list  items.properties (one hop)         = [{properties={gridId=6}}, {properties={gridId=7}}]

The map form on line 1 and the list form on the buggy lines navigate the same key at the same depth; only the container (map vs list) differs, yet the list form drops the data and leaks meta-data.

JUnit reproduction:

java
@Test
public void spreads_nested_properties_over_a_list_of_objects() {
    String json = """
            {
               "items":[
                  { "properties": { "properties": { "gridId": 6 } } },
                  { "properties": { "properties": { "gridId": 7 } } }
               ]
            }""";

    List<Integer> ids = new JsonPath(json).getList("items.properties.properties.gridId", Integer.class);

    assertThat(ids, contains(6, 7)); // fails on master: iterable did not contain <6>
}

Expected behaviour

items.properties.properties.gridId yields [6, 7], mirroring the map form properties.properties.height. No Groovy meta-data (class, empty) is ever exposed as a JsonPath result.

Root cause and suggested fix

Groovy5JsonSlurperWorkarounds.ProxyArray.getProperties() builds the spread with stream().map(...).toList(), which returns a plain immutable List, not a ProxyArray. Because the result is not the proxy type, the properties workaround stops applying after the first list hop.

Collecting the spread into a ProxyArray (the same proxy the parser produces) instead of toList() makes the result carry the workaround forward. I confirmed locally that this returns [6, 7] for the nested case and keeps the existing JsonPathTest suite green.

Source: rest-assured/rest-assured