@bielok/jpath
v0.1.0
Published
A fast, streaming JSON traversal library.
Maintainers
Readme
A fast, streaming JSON traversal library.
This library is new, experimental, and can still be sped up, at least, by a factor of two.
jpath is a streaming JSON path parser that operates on raw byte streams without creating intermediate objects, arrays, or strings. By using chained method calls to define paths and bitwise/ring-buffered structures for state tracking, it minimizes memory footprint and GC pressure, making it ideal for processing large JSON files.
- No runtime dependenies.
- Zero-allocation parsing (no object/array instantiation).
- Streaming input processing (supports chunked data).
- 90%+ test coverage.
- Chained API for defining paths (
.key(),.index(),.any()). - Compatible with both pure JavaScript and TypeScript projects.
- CommonJS, ESM and UMD distributables.
- Includes a fuzzer and concurrency tests.
- Successfully deployed on edge workers using < 128 MiB RAM.
Getting started
Installation
$ npm install @bielok/jpathESM
import { JPath } from "@bielok/jpath";CommonJS
const { JPath } = require("@bielok/jpath");UMD (browser)
<script src="https://unpkg.com/@bielok/jpath/dist/jpath.umd.js"></script>
<script>
const { JPath } = window.jpath;
</script>Example
import { JPath } from "@bielok/jpath";
const p = new JPath();
// Path: $.store.books[0].authors[0]
p.key("store").key("books").index(0).key("authors").index(0);
const json = '{"store":{"books":[{"authors":["Author Name"]}]}}';
const result = p.processChunk(new TextEncoder().encode(json));
// result -> '"Author Name"'API Reference
new JPath(capture?, arena?, nesting?, segments?)
Initializes a new parser instance. All arguments are optional:
| Option | Type | Default | Description |
|---|---|---|---|
| capture | uint | 65536 | Maximum bytes for a single captured value. |
| arena | uint | 16384 | Maximum bytes for path key storage. |
| nesting | uint | 256 | Maximum supported nesting depth. |
| segments | uint | 64 | Maximum number of path segments. |
// Defaults are fine for most cases:
const p = new JPath();
// Tune for deep structures or long paths:
const p = new JPath(65536, 16384, 512, 128);reset()
Resets the parser state for reuse, preserving all pre-allocated buffers. Call this between searches to reconfigure the path without allocating.
p.reset();
p.key("another").key("path");key(string)
Appends an object key segment to the target path. Keys are compared as raw UTF-8
bytes against the incoming JSON stream. Supports escaped characters (\", \\)
inside the key but does not handle \uXXXX unicode escape sequences.
p.key("users").key("name");index(uint)
Appends an array index segment to the target path. Accepts only unsigned integers. Negative indices are not supported.
p.index(0).key("email");any()
Appends a wildcard segment. Matches any object key or any array index (first match wins).
p.key("users").any().key("name");processChunk(Uint8Array)
Processes a chunk of raw JSON bytes. Returns:
- A string: the captured value as raw JSON text (e.g.
'"hello"','42','{"key":"val"}') when the path is found. null: when the path is definitively not found in the stream.undefined: when more data is needed to reach a conclusion.
const result = p.processChunk(bytes) ?? p.end();end()
Signals the end of the input stream. Returns the final captured value if the
path matches, null otherwise. Always call this after the last chunk.
const result: string | null = p.processChunk(bytes) ?? p.end();nextSibling()
Advances the parser to the next sibling match at the same nesting level. After a hit, call this to continue scanning subsequent array elements or object values without descending deeper. This is the building block for streaming iteration — you control when to advance, so you can inspect, skip, or transform each match as it arrives.
const p = new JPath();
p.key("arr").any();
const bytes = new TextEncoder().encode(`{"arr":[10,20,30]}`);
const results: string[] = [];
for (let i = 0; i < bytes.length; i++)
{
const r = p.processChunk(bytes.subarray(i, i + 1));
if (r === null) break;
if (r !== undefined)
{
results.push(r);
p.nextSibling();
}
}
// results: ["10", "20", "30"]nextChild()
Advances the parser to the next match at any depth below the current position.
Unlike nextSibling(), this enables recursive descent — the parser matches the
target path no matter how deeply nested. This is the primitive for deep-scan
queries like $..price in JSONPath, letting you find every occurrence of a key
or pattern anywhere in the document.
const p = new JPath();
p.key("price");
const bytes = new TextEncoder().encode(
`{"store":{"books":[{"price":10},{"price":20}],"name":"shop"},"price":5}`
);
const results: string[] = [];
for (let i = 0; i < bytes.length; i++)
{
const r = p.processChunk(bytes.subarray(i, i + 1));
if (r === null) break;
if (r !== undefined)
{
results.push(r);
p.nextChild();
}
}
// results: ["5", "10", "20"]Both methods are idempotent; calling them when there are no more elements is
safe. The parser returns null when the path is definitively exhausted. If
both are called, the last one wins.
Value Decoding
The parser returns raw JSON text (e.g. '"hello"' or '42'). Use these static
methods to decode captured values into native JS types.
JPath.string(raw)
Decodes a captured JSON string, stripping surrounding quotes and processing
escape sequences. Returns null if the raw value is not a valid JSON string.
JPath.string('"hello\\"world"') // -> 'hello"world'
JPath.string(null) // -> nullJPath.number(raw)
Parses a captured JSON number. Returns null if the raw value is not a valid
number.
JPath.number('42') // -> 42
JPath.number('-3.14') // -> -3.14
JPath.number('"hello"') // -> nullJPath.bool(raw)
Parses a captured JSON boolean or null literal. Returns null if the raw value
is not true, false, or null.
JPath.bool('true') // -> true
JPath.bool('false') // -> false
JPath.bool('null') // -> nullDiagnostics
memoryReport()
Returns the byte size of every pre-allocated buffer owned by this parser instance.
parser.memoryReport()
// -> { total: 102400, buffers: { arena: 16384, cross_buf: 65536, path_spans: 768, is_object_bits: 32, array_indices: 1024 } }Comments
The parser silently skips JSON comments during traversal. This is useful when parsing JSONC (JSON with Comments) or other non-standard JSON dialects.
- Line comments (
//); everything from//to the end of the line is ignored. - Block comments (
/* */); content between/*and*/is ignored. Block comments can span multiple chunks.
{
// This is a line comment
"name": "value",
/* This is a
block comment */
"items": [1, 2, 3]
}Comments are transparent; they do not affect path matching or capture behavior. The parser treats them as whitespace.
Unsupported JSONPath Features
This parser implements a subset of JSONPath focused on streaming extraction. The following standard JSONPath features (RFC 9535) are not supported:
- Filter expressions (
[?(@.price < 10)]); No sub-expression evaluation during traversal. The parser is structural only, not evaluative. - Negative indexing (
[-1]);.index()only accepts unsigned integers. - Union / multi-select (
[0,1]or['name','price']); Each path targets a single key or index.
These limitations exist because the parser operates as a single-pass streaming state machine with zero-allocation constraints. Features requiring sub-expression evaluation or backtracking are architecturally incompatible with this design.
Building & Running tests
$ bun run build # Builds all distributables.
$ bun test # Run unit and concurrency tests.
$ bun run fuzz # Run the random-op fuzzer.Benchmarks
Use the benchmark scripts to run performance tests against baseline implementations.
$ bun run scripts/benchmark.ts
jpath benchmark
cpu: Apple M1 (8 cores)
time: 2026-07-18T17:29:07.401Z
Time from raw bytes to result (competitors pay JSON.parse)
┌───┬─────────────────────┬─────────┬───────────┬─────────────┬─────────────────┐
│ │ path │ jpath │ jsonpath │ jsonpath-js │ jsonpath-faster │
├───┼─────────────────────┼─────────┼───────────┼─────────────┼─────────────────┤
│ 0 │ meta.version │ 0.03 ms │ 219.00 ms │ 235.12 ms │ 222.34 ms │
│ 1 │ meta.stat.val │ 0.04 ms │ 233.24 ms │ 311.15 ms │ 214.32 ms │
│ 2 │ user[0].email │ 0.03 ms │ 262.16 ms │ 235.09 ms │ 223.98 ms │
│ 3 │ user[0].addr.lat │ 0.02 ms │ 261.15 ms │ 217.52 ms │ 216.69 ms │
│ 4 │ user[500].email │ 1.66 ms │ 221.91 ms │ 215.85 ms │ 216.51 ms │
│ 5 │ user[1916].prem │ 6.31 ms │ 223.28 ms │ 215.52 ms │ 214.84 ms │
│ 6 │ user[0].addr │ 0.01 ms │ 235.13 ms │ 297.57 ms │ 234.25 ms │
│ 7 │ first user.email(*) │ 0.01 ms │ 227.67 ms │ 242.92 ms │ 226.11 ms │
└───┴─────────────────────┴─────────┴───────────┴─────────────┴─────────────────┘License
The author disclaims copyright to this source code.
This software is released into the public domain and is provided 'as-is', without warranty of any kind, express or implied.
