JsonPath: array.properties GPath spread returns null (drops all data) when the first array element lacks a properties key (Groovy 5 regression)
Description
For a JSON array of objects, array.properties should behave like Groovy-4 GPath spread, array.collect { it['properties'] }, yielding null for elements that lack the key. The Groovy-5 workaround that restores this gates the entire spread on element [0]: it runs only when get(0) is a Map containing "properties". When the first element has no properties, getProperties() returns null and discards every element's data, with no exception.
JSON features = [ {geometry:{}}, {properties:{gridId:AB}}, {properties:{gridId:CD}} ]
getList("features.properties") => null (all data dropped)
getList("features.properties.gridId") => nullThis is the GeoJSON FeatureCollection shape named in the class's own javadoc. GeoJSON allows a feature's properties to be absent, and feature order is not guaranteed, so one leading feature without properties wipes the whole collection.
rest-assured version
- Regression introduced in 6.0.0 (json-path Java migration, PR #1844 / commit
1d3db214, 2025-12-12); reproduced onmasterHEADeeed4998(6.0.2-SNAPSHOT), Groovy 5.0.3, JDK 24. The pre-migration 5.5.6 line used the old GroovyShell path and is not affected.
Steps to reproduce
import io.restassured.path.json.JsonPath;
import java.util.List;
public class Repro {
public static void main(String[] a) {
// first element has NO "properties"
String bug = "{\"features\":[{\"geometry\":{}},{\"properties\":{\"gridId\":\"AB\"}},{\"properties\":{\"gridId\":\"CD\"}}]}";
System.out.println("first-missing features.properties = " + JsonPath.from(bug).getList("features.properties"));
System.out.println("first-missing features.properties.gridId = " + JsonPath.from(bug).getList("features.properties.gridId"));
// SAME data, only the order differs (properties-less element no longer first)
String ok = "{\"features\":[{\"properties\":{\"gridId\":\"AB\"}},{\"geometry\":{}},{\"properties\":{\"gridId\":\"CD\"}}]}";
System.out.println("first-present features.properties = " + JsonPath.from(ok).getList("features.properties"));
// ordinary (non-'properties') key spreads correctly regardless of order
String foo = "{\"features\":[{\"geometry\":{}},{\"foo\":{\"gridId\":\"AB\"}},{\"foo\":{\"gridId\":\"CD\"}}]}";
System.out.println("ordinary key features.foo = " + JsonPath.from(foo).getList("features.foo"));
}
}Output:
first-missing features.properties = null <-- BUG
first-missing features.properties.gridId = null <-- BUG
first-present features.properties = [{gridId=AB}, null, {gridId=CD}] <-- same data, order swapped: correct
ordinary key features.foo = [null, {gridId=AB}, {gridId=CD}] <-- ordinary key: correctThe cleanest isolation: inputs 1 and 3 have identical key name and data and differ only in array order, yet one returns total null and the other the correct [{gridId=AB}, null, {gridId=CD}]. Swapping only the leaf key properties for foo (same shape) already works, so Groovy 5 handles the ordinary spread; the loss is specific to the properties workaround's first-element guard.
Expected
features.properties = [null, {gridId=AB}, {gridId=CD}] and features.properties.gridId = [AB, CD], per the class javadoc contract (list.collect { it['properties'] }) and the intent of JsonPathTest.automatically_escapes_json_attributes_whose_name_equals_properties().
Actual
null. The whole spread disappears whenever the first element lacks properties.
Root cause
json-path/src/main/java/io/restassured/internal/path/json/Groovy5JsonSlurperWorkarounds.java:57, ProxyArray#getProperties():
public Object getProperties() {
if (!isEmpty() && get(0) instanceof Map<?, ?> entry0 && entry0.containsKey(PROPERTIES)) { // <-- guards on element[0]
return stream().map(elem -> {
if (elem instanceof Map<?, ?> map && map.containsKey(PROPERTIES)) {
return map.get(PROPERTIES);
} else {
return null; // inner body ALREADY tolerates absent/non-map entries
}
}).toList();
}
return null; // <-- first-element guard failed -> WHOLE spread dropped
}The inner lambda already maps absent and non-map entries to null, so the spread handles heterogeneous lists on its own. The sole cause is the get(0) ... containsKey(PROPERTIES) guard, which samples element [0] alone to decide whether to run at all. The existing unit test's fixture has properties on the first feature, so no test exercises this heterogeneous path.
Suggested fix
Gate on the key being present anywhere in the list (or on the list containing any Map), instead of element [0]:
public Object getProperties() {
boolean anyMapHasProperties = stream()
.anyMatch(e -> e instanceof Map<?, ?> m && m.containsKey(PROPERTIES));
if (anyMapHasProperties) {
return stream()
.map(e -> (e instanceof Map<?, ?> m && m.containsKey(PROPERTIES)) ? m.get(PROPERTIES) : null)
.toList();
}
return null;
}Add a regression test with a heterogeneous features array whose first element lacks properties (plus an order-swapped variant), asserting the full spread.
Found via property-based & differential bug-hunting, part of an effort to scale PBT (DepTyCheck-based) testing across the OSS ecosystem.
If this is intended / by-design: I'm really sorry, please just close it — no need to flag or ban me. I'm trying to scale property-based testing across the whole ecosystem and my publishing agents may have gotten this one wrong. I read every issue and follow up on each.
Source: rest-assured/rest-assured