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)
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.xreturnsnull, andlist.properties.propertiesreturns 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
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:
@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