padvinder
v0.2.0
Published
Tiny, CSP-safe, zero-dependency RFC 9535 JSONPath engine. No eval, no new Function.
Maintainers
Readme
padvinder
A tiny, CSP-safe JSONPath engine for JavaScript. ~2.75KB min+gzip, zero dependencies. Passes all 456 valid-selector cases of the official RFC 9535 compliance suite.
Padvinder is Dutch for "pathfinder", and also what we call a scout. It implements RFC 9535 JSONPath, filters included, with a real parser instead of the generated code that filter evaluation usually relies on. There is no eval and no new Function, so a query cannot smuggle code into your application, and the engine runs under a strict Content Security Policy.
import { query, find } from 'padvinder';
const data = {
store: {
book: [
{ title: 'Sayings of the Century', price: 8.95, category: 'reference' },
{ title: 'Sword of Honour', price: 12.99, category: 'fiction' },
{ title: 'Moby Dick', price: 8.99, category: 'fiction' },
],
},
};
// One-shot:
find('$..book[[email protected] < 10].title', data);
// => ['Sayings of the Century', 'Moby Dick']
// Compile once, run many times:
const cheap = query('$.store.book[[email protected] < 10 && @.category == "fiction"].title');
cheap(data); // => ['Moby Dick']
// Register your own filter functions:
find('$..book[?sale(@)].title', data, { sale: b => b.price < 9 });API
query(path, functions?)
Compiles the query and returns a runner (data) => matches[]. Malformed paths and invalid filter expressions throw a SyntaxError at compile time. A query that matches nothing returns an empty array.
find(path, data, functions?)
Shorthand for query(path, functions)(data).
Syntax
| Selector | Meaning |
| --- | --- |
| $ | The root |
| .name, ['name'] | Child property |
| [0], [-1] | Array index, negatives count from the end |
| [1:3], [:2], [-2:], [0:4:2] | Array slice, with optional positive step |
| .*, [*] | All children |
| ..name | Recursive descent: name anywhere below |
| [0,2], ['a','b'] | Union of selectors |
| [?expr], [?(expr)] | Keep children matching the filter (see below) |
Filters
Filters follow the RFC 9535 grammar. Comparisons (==, !=, <, <=, >, >=), the logical operators &&, ||, and !, and parentheses combine two kinds of operand: literals and queries. A bare query is an existence test, so [[email protected]] keeps children that have an a, including a present null. A query used in a comparison is read for its value, and a missing path compares as "nothing" rather than throwing, so [[email protected] == 1] is safe even when a is absent. == is deep structural equality.
find('$.store.book[[email protected] < 10]', data);
find('$.store.book[[email protected] > $.store.bicycle.price]', data); // $ is the root
find('$.store.book[[email protected] == "fiction" && @.price < 20]', data);
find('$.store.book[[email protected]]', data);Five functions ship built in: length(), count(), value(), and the regular-expression tests match() (full match) and search() (substring). Register your own for anything else, and call them from a filter with @ as the current node:
find('$.book[?length(@.title) > 20]', data);
find('$.book[?match(@.isbn, "[0-9]{13}")]', data);
find('$.book[?luhn(@.code)]', data, { luhn: valid }); // your functionContent Security Policy
padvinder works under script-src 'self' with no unsafe-eval. Paths and filters compile to a chain of closures. Query text is never turned into JavaScript.
This matters for JSONPath specifically because filter expressions are the classic weak spot: jsonpath-plus evaluated them by executing generated code, which led to remote code execution via crafted queries (CVE-2024-21534) and follow-up bypasses. padvinder's filters go through a parser that has no route to code execution, so a hostile query can, at worst, return the wrong nodes or throw. The test suite runs under node --disallow-code-generation-from-strings, which throws on any string-to-code construct the same way a strict CSP does.
Safety
- Queries read the data you pass in and never modify it.
__proto__,constructor, andprototypenever match, in paths or in filters. Prototype-chain properties are invisible: matching is own-properties only, so a filter like[[email protected]]finds nothing rather than reachingObject.prototype.- Registered functions resolve only from what you provide, and receive plain data values as arguments.
License
MIT © Robin van der Vleuten
