stream-jsonis a micro-library of Node.js stream components for creating custom JSON processing pipelines with a minimal memory footprint. It can parse JSON files far exceeding available memory streaming individual primitives using a SAX-inspired API. It depends on stream-chain for pipeline composition.
For project structure, module dependencies, and the architecture overview see ARCHITECTURE.md. For detailed usage docs and API references see the wiki.
This project uses a git submodule for the wiki:
git clone --recursive https://github.com/uhop/stream-json.git
cd stream-json
npm install- Install:
npm install - Test:
npm test(runstape6 --flags FO) - Test (Bun):
npm run test:bun - Test (Deno):
npm run test:deno - Test (sequential):
npm run test:seq - Test (process-per-file):
npm run test:proc - Test (single file):
node tests/test-<name>.js - TypeScript check:
npm run ts-check - JS check (checkJs):
npm run js-check - Bench:
npm run bench -- bench/<name>.js - Lint:
npm run lint(Prettier check) - Lint fix:
npm run lint:fix(Prettier write)
stream-json/
├── package.json # Package config; "tape6" section configures test discovery
├── src/ # Source code
│ ├── index.js # Main entry point: creates Parser + emit()
│ ├── index.d.ts # TypeScript definitions for the main module
│ ├── parser.js # Streaming SAX-like JSON parser (token stream)
│ ├── parser.d.ts # TypeScript definitions for parser
│ ├── assembler.js # Token stream → JavaScript objects (plain class, `onDone` callback)
│ ├── assembler.d.ts # TypeScript definitions for assembler
│ ├── disassembler.js # JavaScript objects → token stream
│ ├── disassembler.d.ts # TypeScript definitions for disassembler
│ ├── stringer.js # Token stream → JSON text (flushable function + asStream)
│ ├── stringer.d.ts # TypeScript definitions for stringer
│ ├── emitter.js # Token stream → EventEmitter events (Writable); .asWebStream → EventTarget
│ ├── emitter.d.ts # TypeScript definitions for emitter
│ ├── filters/ # Token stream editors
│ │ ├── filter-base.js # Base for all filters (filterBase + makeStackDiffer)
│ │ ├── pick.js # Pick subobjects by path
│ │ ├── replace.js # Replace subobjects with a value
│ │ ├── ignore.js # Remove subobjects (Replace variant)
│ │ └── filter.js # Filter tokens preserving shape
│ ├── streamers/ # Token stream → object stream
│ │ ├── stream-base.js # Base for all streamers (uses Assembler)
│ │ ├── stream-values.js # Stream successive JSON values
│ │ ├── stream-array.js # Stream array elements
│ │ └── stream-object.js # Stream object properties
│ ├── utils/ # Utilities
│ │ ├── emit.js # Decorate a Node Readable with token events (Web variant at src/web/utils/emit.js → EventTarget)
│ │ ├── with-parser.js # Create parser + component pipelines
│ │ ├── batch.js # Batch items into arrays (wraps stream-chain batch)
│ │ ├── verifier.js # Validate JSON text (gen pipeline + asStream)
│ │ └── flex-assembler.js # Assembler with custom containers (Map, Set, etc.)
│ ├── jsonl/ # JSONL (line-separated JSON) support
│ │ ├── parser.js # JSONL parser → {key, value} objects
│ │ └── stringer.js # Objects → JSONL text (Transform stream; .asWebStream → Web TransformStream)
│ ├── jsonc/ # JSONC (JSON with Comments) support
│ │ ├── parser.js # JSONC parser → token stream (extends parser.js; raw export jsoncParser)
│ │ ├── stringer.js # JSONC token stream → text (extends stringer.js)
│ │ └── verifier.js # JSONC validator with error locations (extends verifier.js; raw export jsoncVerifier)
│ ├── file/ # Node-only file I/O (uses node:fs/promises; NOT mirrored in core/ or web/)
│ │ ├── index.js # Barrel: parseFile, verifyFile, stringerToFile, pipe, drain
│ │ ├── parser.js # parseFile() — file path → token stream (input-edge stage)
│ │ ├── verifier.js # verifyFile() — standalone async validator (Promise<void>)
│ │ ├── stringer.js # stringerToFile() — token stream → file (output-edge sink)
│ │ ├── jsonc/{index,parser,verifier,stringer}.js # JSONC variants
│ │ └── internal/{block-reader,block-writer}.js # Shared async-fs primitives
│ ├── core/ # Pure, substrate-agnostic factories (no Node-stream imports)
│ │ ├── utils/{drain,pipe}.js # New generic helpers — last-value drain + one-shot flush driver
│ │ └── … # Mirrors src/ layout: each component's runtime + .d.ts
│ └── web/ # Web Streams substrate entries (browser-safe)
│ └── … # Mirrors src/ layout: each component's factory + asWebStream + .d.ts
├── tests/ # Test files (test-*.js, using tape-six)
├── bench/ # Micro-benchmarks (nano-benchmark)
├── wiki/ # GitHub wiki documentation (git submodule)
└── .github/ # CI workflows, Dependabot config
- ESM throughout (
"type": "module"in package.json). Runs on currently-supported Node.js. - No transpilation — code runs directly.
- Prettier for formatting (see
.prettierrc): 160 char width, single quotes, no bracket spacing, no trailing commas, arrow parens "avoid". - 2-space indentation.
- Semicolons are enforced by Prettier (default
semi: true). - Imports use
importsyntax with explicit.jsextensions on all relative paths. - Each module exports a default + a named mirror per the fleet's default-export with named mirror convention — the generic name (
parser/verifier/stringer). Parser/verifier modules additionally export the raw inner factory under a format-named export (jsonParser,jsoncParser,jsonlParser,jsonVerifier,jsoncVerifier): the bare tokenizer/validator with nofixUtf8Streamfront, so the default/parserisgen(fixUtf8Stream(), jsonParser()). Stringers (no UTF-8 front) exportstringerplus a format-named alias. - The package is
stream-json. It depends onstream-chain4.x for pipeline composition. - Comments are why-markers only — a non-trivial decision or constraint, an algorithm reference, or explicitly requested JSDoc; never narrate what the code does (fleet convention
no-narrating-comments).
- One runtime dependency:
stream-chain. Do not add other packages todependencies. OnlydevDependenciesare allowed. - Do not modify or delete test expectations without understanding why they changed.
- Keep
.jsand.d.tsfiles in sync for all modules undersrc/. - Token-based architecture. The parser produces a stream of
{name, value}tokens. All filters, streamers, and utilities operate on this token protocol. - Backpressure must be handled correctly. All stream components rely on Node.js stream infrastructure via
stream-chain. - Intended input is data the user owns or trusts (dumps, exports, logs). The library is not designed for hostile input; docs say so, and code changes are not hardened against adversarial JSON or JSONC beyond
JSON.parseparity (__proto__becomes an own property) and the filters'maxDepthguard.
- Parser (
src/parser.js) is the core. It consumes text and produces a SAX-like token stream:{name: 'startObject'},{name: 'keyValue', value: 'key'},{name: 'stringValue', value: '...'}, etc.- Uses
stream-chain'sgen(),flushable(),many(),none,fixUtf8Stream, andasStream. The default/parserisgen(fixUtf8Stream(), jsonParser()); the namedjsonParseris the raw inner tokenizer (charCodeAtclassification + whole-lexeme fast paths, falling back to an incremental regex machine). - Options:
packKeys,packStrings,packNumbers,streamKeys,streamStrings,streamNumbers,jsonStreaming.
- Uses
- Assembler (
src/assembler.js, implementation insrc/core/assembler.js) interprets the token stream and reconstructs JavaScript objects. Plain class — noEventEmitterinheritance in 3.x.- Used internally by all streamers via
streamBase. - Reads only packed tokens (
keyValue,stringValue,numberValue); streamed chunks are ignored. - Materializes like
JSON.parse: a__proto__key becomes an own property viaObject.defineProperty, never the prototype (plain assignment would hit the inherited setter). Same inFlexAssembler. Assembler.connectTo(stream, {onDone: asm => …})is substrate-aware: accepts either a NodeReadable(attaches'data'listener) or a WebReadableStream(pumps viagetReader()). Detection viatypeof stream.getReader === 'function'.asm.onDone(fn)can set/clear the callback after construction.- For hot paths, prefer a manual
for await (const tok of readable) asm.consume(tok)loop overconnectTo— no async-closure overhead, errors propagate directly.FlexAssemblerhas the same shape. asm.tapChainis a function for use inchain().
- Used internally by all streamers via
- Disassembler (
src/disassembler.js) does the inverse: JS objects → token stream. - Stringer (
src/stringer.js) converts a token stream back to JSON text. Functional:flushable+asStream(). - Emitter (
src/emitter.js) factory returning aWritablethat re-emits tokens as named EventEmitter events (subscribe with.on(name, fn)). Web counterpart (src/web/emitter.js) returns anEventTargetwith a.writableWritableStreamattached; each token dispatches as aCustomEvent(name, {detail: value})(subscribe with.addEventListener(name, ev => ev.detail)). EventTarget + CustomEvent are universal across modern Node, Bun, Deno, and browsers. For hot paths, prefer thefor awaitform over the emitter — same model, no per-tokenCustomEventallocation, no listener-registry indirection. - Filters (
src/filters/) edit the token stream:pick,replace,ignore,filter. All built onfilterBase.filterBaseprovides a state machine that tracks JSON path stack and applies accept/reject actions.makeStackDiffergenerates structural tokens to reconstruct the surrounding JSON envelope.
- Streamers (
src/streamers/) assemble complete JS objects from the token stream:streamValues,streamArray,streamObject. All built onstreamBase.streamBaseusesAssemblerinternally and supportsobjectFilterfor early rejection.
- Utilities:
emit(),withParser(),batch,verifier,FlexAssembler.FlexAssembler(src/utils/flex-assembler.js) is a standalone clone ofAssemblerthat supports custom containers via path-matching rules. Same API surface as Assembler.withParser(fn, options)creates agen(parser(options), fn(options))pipeline — the most common pattern.- Most components export
.withParser(options)and.withParserAsStream(options)static methods.
- JSONL (deprecated — slated for removal in a future major):
jsonl/parser.jsandjsonl/stringer.jsare thin re-exports of stream-chain's JSONL (the parser API was absorbed into stream-chain). The Node/Web wrappers delegate.asStream/.asWebStreamto stream-chain's bundledstream-chain/node/jsonl/*andstream-chain/web/jsonl/*factories. Use stream-chain's JSONL directly. Rationale: stream-json is a JSON token library; JSONL yields whole objects per line and belongs in stream-chain with the other substrate components. - JSONC:
jsonc/parser.js,jsonc/stringer.js, andjsonc/verifier.jsfor JSON with Comments. Extend the standard parser/stringer/verifier (samecharCodeAttokenizer/validator) withwhitespace/ comment /commatokens, trailing comma support, andstreamWhitespace/streamComments/packComments/streamCommas(parser) plususeCommentValues/useCommas(stringer) options. Comments mirror strings (startComment/commentChunk/endComment, packedcommentValue) and their scan resumes across chunks — never rescan an accumulated comment from its start.streamCommas+useCommasgive byte-faithful comma round-trips (incl. trailing commas) for streaming edits; both default off. Raw inner exports:jsoncParser,jsoncVerifier. - File I/O (Node-only) (
src/file/, since 3.3.0):parseFile()is an input-edge stage that turns a file path into a token stream (gen(asyncBlockReader, jsonParser));stringerToFile(path)is the symmetric output-edge sink (gen(stringer, asyncBlockWriter));verifyFile(path)is a standalone async validator returningPromise<void>. JSONC variants undersrc/file/jsonc/. NOT mirrored incore/orweb/because they usenode:fs/promises. Compose withpipe(...)(one-shot driver with auto-flush —gen(...)alone doesn't flush, sostringerToFilewouldn't close the file) anddrain(asyncGen)(returns the last yielded value orundefined). Both helpers live incore/utils/(web-safe, no Node deps). - Substrate split:
src/core/holds the pure substrate-agnostic factories (no Node-stream imports — checked bytests/node/test-browser-safe.jswhich scans.d.tsfornode:*imports andextends DuplexOptions).src/(Node entry) attaches.asStream(Node Duplex) and.asWebStream(Web{readable, writable}pair).src/web/attaches only.asWebStreamand.withParserAsWebStream, with no Node-stream imports — safe for browser bundles. Thechainfromstream-chain(Node) orstream-chain/web(Web) auto-wraps the pure flushables on both substrates, so user-facing pipeline code is identical.
import test from 'tape-six';
import {parser} from '../src/index.js';
import {streamArray} from '../src/streamers/stream-array.js';
import chain from 'stream-chain';
import {Readable} from 'node:stream';
test('example', async t => {
const output = [];
const pipeline = chain([Readable.from(['[1, 2, 3]']), parser(), streamArray()]);
pipeline.on('data', item => output.push(item));
await new Promise(resolve => pipeline.on('end', resolve));
t.deepEqual(
output.map(o => o.value),
[1, 2, 3]
);
});- Test files use
tape-six:.jsfor runtime tests,.tsfor typed tests. - Test file naming convention:
test-*.js(ortest-types-*.ts) intests/. - Tests are configured in
package.jsonunder the"tape6"section. - Property-based tests live in
tests/node/test-property-parser.js(tape-six-fast-check,t.prop()over fast-check arbitraries): chunk-boundary invariance for the JSON and JSONC parsers and the two round-trips. A new streaming invariant belongs there, not in an example-based file. - Test files should be directly executable:
node tests/test-foo.js.
The parser emits these token types:
| Token name | Value | Meaning |
|---|---|---|
startObject |
— | { encountered |
endObject |
— | } encountered |
startArray |
— | [ encountered |
endArray |
— | ] encountered |
startKey |
— | Start of object key string |
endKey |
— | End of object key string |
keyValue |
string | Packed key value |
startString |
— | Start of string value |
endString |
— | End of string value |
stringChunk |
string | Piece of a string |
stringValue |
string | Packed string value |
startNumber |
— | Start of number |
endNumber |
— | End of number |
numberChunk |
string | Piece of a number |
numberValue |
string | Packed number (as string) |
nullValue |
null | null literal |
trueValue |
true | true literal |
falseValue |
false | false literal |
Token is a discriminated union over name; TokenName is the closed name set. Stage shapes are named types on the parser entries: TokenSource (text → tokens), TokenTransform (tokens → tokens), TokenConsumer<Item> (tokens → items), TokenStringer (tokens → text). Streamer items are KeyedValue<K, T> (streamers/stream-base.js) — K is string for streamObject, number for streamArray/streamValues.
- The only runtime dependency is
stream-chain. Do not add others. - All public API is in
src/. Keep.jsand.d.tsfiles in sync. - Wiki documentation lives in the
wiki/submodule. - Most components follow the factory pattern:
import {pick} from 'stream-json/filters/pick.js'. - Components that work with a parser typically export
.withParser()and.withParserAsStream(). - The
Assembler.tapChainproperty returns a function suitable for use inchain().
- Start with
ARCHITECTURE.mdfor the module map and dependency graph. src/parser.jsis the core — read it first to understand the token protocol.src/filters/filter-base.jsis the foundation for all filters — read it to understand path matching.src/streamers/stream-base.jsis the foundation for all streamers — read it to understand object assembly.- Wiki markdown files in
wiki/contain detailed usage docs.