@sdxc/jsonpath
RFC 9535 JSONPath queries over JSON values, with normalized paths and every function extension
- Installs with
- @sdxc/cli@sdxc/result
- Depends on
remix- Source
- packages/jsonpath
RFC 9535 JSONPath queries over JSON values, with normalized paths and every function extension.
JSONPath selects nodes from a JSON value with
expressions like $.store.book[?@.price < 10].title. This package implements the whole standard:
name, wildcard, index, slice and filter selectors; child and descendant segments; comparisons and
logical operators; and the length, count, match, search and value functions, with
match and search reading I-Regexp patterns. It passes
the official JSONPath compliance test suite.
Installation
npm add @sdxc/jsonpathFallible calls return @sdxc/result values.
Usage
Run a query once
import { query } from "@sdxc/jsonpath";
query({ users: [{ name: "Ada", admin: true }, { name: "Alan" }] }, "$.users[?@.admin].name");
// success([{ value: "Ada", location: ["users", 0, "name"], path: "$['users'][0]['name']" }])Parse once, select many times
import { JSONPath } from "@sdxc/jsonpath";
import { unwrap } from "@sdxc/result";
let cheap = unwrap(JSONPath.parse("$..book[?@.price < 10].title"));
cheap.values(store); // ["Sayings of the Century", "Moby Dick"]
cheap.values(otherStore);Functions
import { JSONPath } from "@sdxc/jsonpath";
import { unwrap } from "@sdxc/result";
unwrap(JSONPath.parse("$.books[?length(@.title) > 15]"));
unwrap(JSONPath.parse("$.books[?count(@.authors[*]) >= 2]"));
unwrap(JSONPath.parse("$.users[?match(@.email, '[a-z]+@example\\.com')]"));
unwrap(JSONPath.parse("$.books[?search(@.title, 'Rings')]"));
unwrap(JSONPath.parse("$.groups[?value(@..id) == 7]"));In match and search patterns, . matches anything but a line break, and ^ and $ anchor as
the compliance suite expects. A pattern that is not an I-Regexp makes the function false.
Command line
bunx @sdxc/jsonpath query '$.store.book[?@.price < 10].title' store.jsonWith @sdxc/kit installed, sdxc pkg jsonpath … runs the same command line, using the version your project installs.
query prints the matched values as a JSON array, reading the document from a file or from
stdin. --paths prints normalized paths instead, --first prints the first match alone, and
--fail-empty exits 1 when nothing matches. An invalid expression exits 2 and points at the
offset where it fails. bunx @sdxc/jsonpath --help lists every flag.
cat package.json | bunx @sdxc/jsonpath query '$.name' --firstAPI
JSONPath.parse(source: string): Result<JSONPath, JsonPathSyntaxError>
Parses a query, refusing everything RFC 9535 refuses: invalid syntax, whitespace before or after
the query, an index outside ±(2^53 − 1), an unknown function, and a function argument of the wrong
type, such as length(@.*) or comparing a query that can select several nodes.
jsonPath.select(document: JSONValue): JsonPathNode[]
The selected nodes, in the order RFC 9535 defines. Object members are visited in the order the object holds them.
jsonPath.values(document: JSONValue): JSONValue[]
The selected values alone.
jsonPath.source
The query text.
query(document: JSONValue, source: string): Result<JsonPathNode[], JsonPathSyntaxError>
JSONPath.parse and select in one call.
normalizedPath(location: readonly (string | number)[]): string
The normalized path of a location, the canonical query for one node: ["a", 0] becomes
$['a'][0], with ', \ and control characters escaped.
JsonPathSyntaxError
The parse failure; offset is where in the query the problem was found.
Types
JsonPathNode
{ value, location, path }: the selected value, its member names and indexes from the root, and
its normalized path.
JSONValue
The JSON values a query reads.
Pattern: Patch Every Matching Node
location is the node's path as segments, which
@sdxc/json-pointer turns into the pointers
@sdxc/json-patch operations aim at.
import { apply } from "@sdxc/json-patch";
import { stringify } from "@sdxc/json-pointer";
import { JSONPath } from "@sdxc/jsonpath";
import { unwrap } from "@sdxc/result";
let overdue = unwrap(JSONPath.parse("$.invoices[?@.due < '2026-01-01' && !@.paid].status"));
let patch = overdue.select(ledger).map((node) => ({
op: "replace" as const,
path: stringify(node.location),
value: "overdue",
}));
let next = apply(ledger, patch);Pattern: Let Clients Filter A Collection
A query string carries a JSONPath filter; the handler parses it once and answers 400 with the
offset when it is invalid.
import { JSONPath } from "@sdxc/jsonpath";
import { isFailure } from "@sdxc/result";
async function listOrders(request: Request) {
let filter = new URL(request.url).searchParams.get("filter") ?? "$[*]";
let parsed = JSONPath.parse(filter);
if (isFailure(parsed)) {
return Response.json(
{ error: parsed.error.message, offset: parsed.error.offset },
{ status: 400 },
);
}
return Response.json(parsed.data.values(await orders.all()));
}