For nearly two decades, Stefan Gössner’s original 2007 blog post served as the de facto specification for JSONPath. Because that original specification left dozens of syntax and semantic edge cases ambiguous, library authors in Python (jsonpath-ng), Node.js (jsonpath-plus), Java (Jayway JsonPath), Go, and Kubernetes filled the gaps with their own heuristics.
In 2024, the IETF standardized JSONPath as RFC 9535. However, if you build microservices, data ingestion pipelines, or event routers where JSONPath queries written for one system run in another, you have almost certainly encountered silent extraction bugs or unexpected empty arrays.
Here are five subtle JSONPath edge cases that break in production across different implementations and how RFC 9535 resolves them.
1. The null Value vs. Missing Key Ambiguity in Filters
Consider this payload:
{
"products": [
{ "id": 1, "name": "Mechanical Keyboard", "discount": 15 },
{ "id": 2, "name": "USB-C Cable", "discount": null },
{ "id": 3, "name": "Desk Mat" }
]
}
If you query $.products[?@.discount], legacy libraries disagree on product #2. Some treat null as falsey (like JavaScript or Python bool(None)), returning only product #1. Others evaluate whether the key exists in the object, returning both product #1 and product #2.
Under RFC 9535, null is a first-class JSON value. An existence query ?@.discount evaluates to true for both #1 and #2 because the member exists. If you only want products with an active numerical discount, you must write an explicit comparison: $.products[?@.discount > 0].
2. Deep Scan (..) Traversal Order and Array Indexing
What does $..items[0] return on nested structures?
{
"department": "Engineering",
"items": ["laptop", "monitor"],
"subteams": [
{ "name": "Infra", "items": ["server", "switch"] }
]
}
Legacy engines disagree on whether $..items[0] returns:
- The first item in every matching array encountered during traversal (
["laptop", "server"]). - Only the first item from the top-level items array (
"laptop"). - The first item after flattening all matched collections.
RFC 9535 defines recursive descent (..) as yielding nodes in depth-first preorder traversal. When combining recursive descent with child selectors, each visited node is evaluated independently, but older libraries often mix up index evaluation order across depths.
3. Filter Evaluation: eval() Security Traps vs. Native Logic
In older JavaScript and Python JSONPath libraries, filter expressions like [?(@.price < 50 && @.category == 'books')] were evaluated using host-language runtime eval() or Function() constructors. This created critical security vulnerabilities where malicious JSONPath expressions could trigger remote code execution.
Beyond security, it introduced subtle language-specific behaviors (such as JavaScript's loose type coercion where "" == 0 is true).
RFC 9535 explicitly forbids host-language expression evaluation. Standard filter expressions now only support strict value comparisons (==, !=, <, <=, >, >=) and logical operators (&&, ||, !), ensuring predictable results regardless of the host language runtime.
When debugging multi-level filter expressions and testing nested extractions across services, you can verify your query logic with interactive tools like the Nutilz JSONPath Evaluator, which lets you inspect extracted values, node counts, and resolved paths against complex payloads without writing ad-hoc test scripts.
4. Regex Pattern Matching: Custom Flags vs. search() / match()
Legacy implementations often invented their own regex syntax inside filter expressions, such as [?(@.sku =~ /^PROD-[0-9]+/i)]. Because regex engines vary between PCRE, Python re, and ECMAScript, queries copied from documentation frequently failed in production.
RFC 9535 standardizes regex matching through built-in filter functions:
-
match(path, pattern): Matches against the entire string (implicitly anchored with^and$). -
search(path, pattern): Matches anywhere in the target string.
Both functions require the I-RegExp (RFC 9485) standard, preventing catastrophic backtracking (ReDoS) issues in untrusted queries.
5. Negative Slices and Array Bounds
Array slicing syntax like $[start:end:step] seems intuitive, but negative indices expose differences:
["a", "b", "c", "d", "e"]
Query: $[-3:-1]
In RFC 9535:
- A negative index
-kis normalized tomax(0, length - k). - For length 5,
start = max(0, 5 - 3) = 2(value"c"), andend = max(0, 5 - 1) = 4(value"e"). - The slice extracts index 2 and 3:
["c", "d"].
Some legacy engines incorrectly clamped negative bounds or failed to handle step directions properly when step < 0 (such as $[4:1:-1]), causing off-by-one errors that silently drop the first or last element of an array in production pipelines.
Summary Checklist for Production JSONPath
-
Avoid truthiness shortcuts: Use explicit comparisons (
@.status == 'active') rather than relying on boolean coercion. -
Replace vendor regex syntax: Migrate custom
=~operators to standardmatch()orsearch()functions. - Validate across your toolchain: Test your queries against real payloads using testing tools like nutilz.com or RFC 9535 test runners before deploying them to production pipelines.
Top comments (0)