JSONPath: Filter an Array by a Nested Field and Multiple Conditions
To filter an array of objects by a nested field in JSONPath, put a filter expression on the array and reach into each element with @: $.orders[?(@.customer.tier == 'gold')]. Combine conditions inside the same ?( ) with &&, || and !. If some elements lack an intermediate object, guard the path, because in the playground a single missing object makes the whole query fail.
All outputs below come from the JSONPath playground, which uses jsonpath-plus 7.2.0. That library evaluates the text inside ?( ) as a JavaScript expression. Other JSONPath libraries, and the RFC 9535 standard, behave differently in places. The last section covers the differences that matter here.
Sample data
Every example uses this document. Note that order A-103 has no address, and A-104 has an empty coupon:
{
"orders": [
{
"id": "A-100",
"status": "shipped",
"total": 240,
"customer": {
"name": "Ana",
"tier": "gold",
"address": {
"country": "DE"
}
}
},
{
"id": "A-101",
"status": "pending",
"total": 35,
"customer": {
"name": "Ben",
"tier": "gold",
"address": {
"country": "US"
}
}
},
{
"id": "A-102",
"status": "shipped",
"total": 180,
"customer": {
"name": "Chloe",
"tier": "silver",
"address": {
"country": "DE"
}
}
},
{
"id": "A-103",
"status": "cancelled",
"total": 410,
"customer": {
"name": "Dev",
"tier": "gold"
}
},
{
"id": "A-104",
"status": "shipped",
"total": 95,
"customer": {
"name": "Eli",
"tier": "silver",
"address": {
"country": "FR"
}
},
"coupon": ""
}
]
}
Filter by a nested field
@ is the current array element, so @.customer.tier reads the tier of each order's customer. Append .id after the filter to return just the matching IDs instead of whole objects:
$.orders[?(@.customer.tier == 'gold')].id
Output:
[
"A-100",
"A-101",
"A-103"
]
Run the nested-field filter in the playground
Drop the trailing .id to get the full matching order objects, or end with another path such as .customer.name.
Combine conditions with &&, || and !
Both conditions go inside one filter. Gold customers with an order total over 100:
$.orders[?(@.customer.tier == 'gold' && @.total > 100)].id
Output:
[
"A-100",
"A-103"
]
Run the combined filter in the playground
Use || for either condition, and !( ) to negate one. Parentheses make the grouping explicit:
$.orders[?(@.status == 'pending' || @.total > 200)].id
["A-100","A-101","A-103"]
$.orders[?(!(@.status == 'cancelled'))].id
["A-100","A-101","A-102","A-104"]
Run the || filter in the playground
Missing nested fields: guard the path
Filtering German orders looks simple, but order A-103 has no address:
$.orders[?(@.customer.address.country == 'DE')].id
The playground does not skip that element. It returns an error and no results at all:
jsonPath: Cannot read properties of undefined (reading 'country'): @.customer.address.country == 'DE'
Open the failing filter in the playground
Because the filter is evaluated as JavaScript, reading .country from undefined throws. Guard the intermediate object, either with optional chaining or with an explicit existence check:
$.orders[?(@.customer.address?.country == 'DE')].id
$.orders[?(@.customer.address && @.customer.address.country == 'DE')].id
Both return:
[
"A-100",
"A-102"
]
Run the guarded filter in the playground
A missing last field is not a problem: in @.coupon == '', an order without coupon compares undefined with '' and is simply excluded. Only a missing object in the middle of the path throws.
Test whether a field exists
?(@.field) keeps elements whose value is truthy, not elements where the key exists. A-104 has "coupon": "", and an empty string is falsy:
$.orders[?(@.coupon)].id
[]
$.orders[?(@.coupon !== undefined)].id
["A-104"]
$.orders[?(!@.customer.address)].id
["A-103"]
- The first filter returns nothing:
A-104has a coupon key, but""is falsy, so it is dropped. !== undefinedmatches any order that has the key, whatever its value.!@.customer.addressfinds the orders that are missingaddress.
The same applies to 0, false and null values. Compare with !== undefined when the key's presence is what matters.
Run the existence check in the playground
jsonpath-plus versus RFC 9535
JSONPath was standardized as RFC 9535 in 2024, and libraries differ. Check these points before you copy an expression into another tool:
- Parentheses. RFC 9535 writes filters as
[?@.total > 100]. In jsonpath-plus 7.2.0 that form returns[]without an error, so the playground needs[?(@.total > 100)]. - Missing paths. Under RFC 9535, a comparison against a missing value is false and the element is skipped. In jsonpath-plus, a missing intermediate object throws, as shown above.
- JavaScript-only syntax.
?.,===,!==andundefinedwork here only because the filter is JavaScript. They are not standard JSONPath. Also,==uses JavaScript loose equality, so@.total == '240'matches the number240. Use===when types matter.
If you need to filter and reshape the result, for example to build new objects or handle missing values with defaults, jq is a better fit. See how to check whether a key exists in jq.