JsonPath: array.properties GPath spread returns null (drops all data) when the first array element lacks a properties key (Groovy 5 regression)

Author: haskiindahouseCreated Jul 21, 2026Updated Jul 21, 2026

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")  =>  null

This 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 on master HEAD eeed4998 (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

java
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: correct

The 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():

java
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]:

java
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