stream-json is 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. It has one runtime dependency — stream-chain for pipeline composition.
3.x adopts a tri-tree structure mirroring stream-chain v4 — core/ for
stream-agnostic factories, top-level src/ for the Node-flavored wrappers
that attach .asStream + .asWebStream, and src/web/ for browser-leaning
wrappers that attach only .asWebStream. Consumers pick the entry path that
matches their runtime; bundlers only walk what's actually imported.
src/ # Source code
├── core/ # Pure, stream-agnostic factories — no `node:*` imports
│ ├── parser.{js,d.ts}
│ ├── assembler.{js,d.ts} # plain class (no EventEmitter inheritance)
│ ├── disassembler.{js,d.ts}
│ ├── stringer.{js,d.ts}
│ ├── filters/{filter-base,filter,pick,ignore,replace}.{js,d.ts}
│ ├── streamers/{stream-base,stream-array,stream-object,stream-values}.{js,d.ts}
│ ├── utils/{batch,verifier,with-parser,flex-assembler}.{js,d.ts}
│ ├── jsonl/{parser,stringer}.{js,d.ts}
│ └── jsonc/{parser,stringer,verifier}.{js,d.ts}
│
├── index.{js,d.ts} # Node entry: re-exports parser/parserStream from ./parser.js
├── parser.{js,d.ts} # Node wrapper: attaches BOTH .asStream + .asWebStream
├── assembler.{js,d.ts} # Re-export of core/assembler.js (plain class is portable)
├── disassembler.{js,d.ts} # Node wrapper
├── stringer.{js,d.ts} # Node wrapper
├── emitter.{js,d.ts} # Node Writable that re-emits each token as a named event (+ .asWebStream → EventTarget)
├── filters/{…}.{js,d.ts} # Node wrappers
├── streamers/{…}.{js,d.ts}# Node wrappers
├── utils/ # Node wrappers
│ ├── emit.{js,d.ts} # Decorates a Node Readable with token-named events (Web variant at src/web/utils/emit.js)
│ ├── batch.{js,d.ts}
│ ├── verifier.{js,d.ts}
│ ├── with-parser.{js,d.ts}
│ └── flex-assembler.{js,d.ts} # Re-export of core/utils/flex-assembler.js
├── jsonl/ # Node wrappers
│ ├── parser.{js,d.ts}
│ └── stringer.{js,d.ts}
├── jsonc/ # Node wrappers
│ ├── parser.{js,d.ts}
│ ├── stringer.{js,d.ts}
│ └── verifier.{js,d.ts}
├── file/ # Node-only file I/O — NOT mirrored in core/ or web/ (uses node:fs)
│ ├── index.{js,d.ts} # Barrel: parseFile, verifyFile, stringerToFile, pipe, drain
│ ├── parser.{js,d.ts} # parseFile() — file path → token stream (input-edge stage)
│ ├── verifier.{js,d.ts} # verifyFile() — standalone async validator (Promise<void>)
│ ├── stringer.{js,d.ts} # stringerToFile() — token stream → file (output-edge sink)
│ └── jsonc/{index,parser,verifier,stringer}.{js,d.ts} # JSONC variants
│
└── web/ # Web entry: attaches only .asWebStream — no Node imports walked
├── index.{js,d.ts} # Web main entry: parserWebStream
├── parser.{js,d.ts} # Web wrapper for parser
├── disassembler/stringer/filters/*/streamers/*/utils/{batch,verifier,with-parser}/jsonl/parser/jsonc/*
└── emitter.{js,d.ts}, utils/emit.{js,d.ts} — EventTarget-based equivalents; full mirror of the Node tree
tests/ # Test files (test-*.js, using tape-six)
bench/ # Micro-benchmarks (nano-benchmark)
wiki/ # GitHub wiki documentation (git submodule)
.github/ # CI workflows, Dependabot config
Three-entry rule: each portable component lives in three forms — pure
factory in core/, Node wrapper in src/ proper, Web wrapper under src/web/.
The Node wrapper attaches BOTH .asStream (Node Duplex) and .asWebStream
(Web {readable, writable} pair) since modern Node and Bun support both stream
flavors natively. The Web wrapper attaches only .asWebStream so a
browser-only bundle pulls no Node-stream code.
SAX-event helpers have substrate-specific shapes. The Node emitter.js
extends Writable (an EventEmitter) so consumers subscribe with .on(name, fn).
The Web emitter.js returns an EventTarget with a .writable WritableStream
attached; consumers subscribe with .addEventListener(name, ev => ev.detail).
Same model — token-name as event name, token-value as event payload — different
substrate APIs. utils/emit.js has the same Node/Web split. EventTarget +
CustomEvent are universal across modern Node, Bun, Deno, and browsers, so no
polyfill is needed.
The parser produces a stream of {name, value} tokens — a SAX-inspired protocol:
| 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 |
All downstream components (filters, streamers, stringer, emitter) consume and/or produce tokens in this format. This is the universal interchange protocol of the library.
The typings name the stage shapes as exported aliases on the parser entries (core/parser.d.ts, re-exported by the Node and Web wrappers): TokenSource (text → tokens), TokenTransform (tokens → tokens), TokenConsumer<Item> (tokens → items), TokenStringer (tokens → text). Component factories declare their returns in these terms.
parser(options)returns agen(fixUtf8Stream(), jsonParser(options))pipeline — a function for use inchain().parser.asStream(options)wraps that pipeline as a Duplex stream viaasStream().- The inner
jsonParseris aflushable()function that maintains a state machine. It buffers incoming text and produces{name, value}tokens as amany()array. The tokenizer classifies structure and scans short strings, keys, and numbers withcharCodeAt+ whole-lexeme fast paths, falling back to an incremental regex state machine for escapes, long or cross-chunk lexemes, and literals. The innerjsonParseris also a public named export — the raw tokenizer without thefixUtf8Stream()front. The sameparser(gen) +<fmt>Parser(raw) split applies to the JSONC and JSONL parsers and to the verifiers (verifier+jsonVerifier/jsoncVerifier); stringers, having no UTF-8 front, exportstringerplus a format-named alias. - Parser options control packing and streaming of keys, strings, and numbers:
packKeys/packStrings/packNumbers(default: true) — emitkeyValue/stringValue/numberValuetokens with the complete value.streamKeys/streamStrings/streamNumbers(default: true) — emitstart*/*Chunk/end*tokens for incremental processing.packValues/streamValues— shortcut to set all three at once.jsonStreaming— support multiple top-level values (JSON Streaming protocol).
Assembler is a plain class (no EventEmitter base) that interprets the token stream and reconstructs JavaScript objects:
Assembler.connectTo(stream, {onDone})— accepts either a NodeReadableor a WebReadableStream; detects the substrate viatypeof stream.getReader === 'function'and either pumps viagetReader()(Web) or listens on'data'(Node). TheonDone(asm)callback fires when a top-level value is assembled. The 2.xEventEmittershape (asm.on('done', …)) is removed in 3.0 — use theonDoneoption orasm.onDone(fn).asm.tapChain— a function for use inchain()that returns assembled values ornone.- Tracks
depth,path,current,key,stack. - Writes keys like
JSON.parse: a__proto__key becomes an own data property (Object.defineProperty); plain assignment would invoke the inherited setter and replace the object's prototype.FlexAssemblerdoes the same for plain objects. - Supports
reviveroption (likeJSON.parsereviver) andnumberAsString.
The inverse of Assembler: takes JavaScript objects and produces a token stream via a generator function. Supports replacer, packKeys/packStrings/packNumbers, streamKeys/streamStrings/streamNumbers.
A flushable function that converts a token stream back into JSON text. Handles comma insertion, depth tracking, string escaping. Supports useValues/useKeyValues/useStringValues/useNumberValues to choose between packed and streamed tokens. makeArray option wraps output in []. Use stringer() in chain() or stringer.asStream() for .pipe().
A factory function returning a Writable stream that re-emits each token as a named event: e.on('startObject', ...), etc. Pattern exception: since it's a stream endpoint that emits events, it returns a Writable directly rather than a plain function.
All filters are built on filterBase (src/filters/filter-base.js):
filterBase({specialAction, defaultAction, nonCheckableAction, transition})returns a factory that acceptsoptionsand returns aflushable()function.- It maintains a path stack tracking the current JSON position.
filteroption: a string, RegExp, or function(stack, chunk) → booleanthat determines whether to accept or reject each subobject.makeStackDiffergenerates structural tokens (start/end object/array, key tokens) to reconstruct the surrounding JSON envelope when filtering.- Keys are tracked only from
keyValuetokens, so key-based paths and parent recreation need packed keys from upstream (the parser's default). Replayed parent keys are always packed; their streamed form mirrors upstream unlessstreamKeysis set.
| Filter | specialAction | defaultAction | Effect |
|---|---|---|---|
pick |
accept |
ignore |
Passes only matching subobjects |
replace |
reject |
accept-token |
Replaces matching subobjects |
ignore |
reject |
accept-token |
Removes matching subobjects |
filter |
accept/accept-token |
ignore |
Keeps matching, preserves structure |
All streamers are built on streamBase (src/streamers/stream-base.js):
streamBase({push, first, level})returns a factory that acceptsoptionsand returns a function for use inchain().- Uses
Assemblerinternally to reconstruct objects; reads only packed tokens (keyValue,stringValue,numberValue), the parser's default. levelcontrols when to emit: level 0 forstreamValues, level 1 forstreamArray/streamObject.objectFilteroption enables early rejection: ifobjectFilter(asm)returnsfalse, the object is abandoned without completing assembly.firstcallback validates the opening token (e.g.,streamArrayrequiresstartArray).
| Streamer | Level | Output | Expects |
|---|---|---|---|
streamValues |
0 | {key: index, value: ...} |
Any JSON values in sequence |
streamArray |
1 | {key: index, value: ...} |
Single top-level array |
streamObject |
1 | {key: string, value: ...} |
Single top-level object |
The item shape is the exported KeyedValue<K, T> type (core/streamers/stream-base.d.ts): K is string for streamObject, number for streamArray and streamValues; the per-streamer StreamXxxItem<T> types are aliases of it.
emit(stream)— attaches a'data'listener that re-emits each token as a named event on the stream.withParser(fn, options)— createsgen(parser(options), fn(options)). Most components export.withParser()and.withParserAsStream()static methods.batch— Groups items into fixed-size arrays (default 1000). Wrapsstream-chain/utils/batch. Usebatch()inchain()orbatch.asStream()for.pipe().verifier— Validates JSON text and reports exact error position (offset, line, pos), using the samecharCodeAtclassification and whole-lexeme fast paths as the parser. Composed asgen(fixUtf8Stream(), jsonVerifier()); the raw inner validator is the named exportjsonVerifier. Useverifier()inchain()orverifier.asStream()for.pipe().
Both modules are thin proxies to
stream-chain/jsonl/*and are slated for removal in a future major version. Use stream-chain's JSONL directly. stream-json is a JSON token library; JSONL yields whole objects per line and belongs in stream-chain with the other substrate components that were extracted out of stream-json. The parser API (incl.reviver/errorIndicator) was absorbed into stream-chain; the Node/Web wrappers delegate.asStream/.asWebStreamto stream-chain 4.2.1's bundledstream-chain/node/jsonl/*andstream-chain/web/jsonl/*factories.
jsonl/parser.js— re-export ofstream-chain/jsonl/parser.js(pure factory, composed asgen(fixUtf8Stream(), lines(), jsonlParser())); the namedjsonlParseris the raw per-line parse function..asStream/.asWebStreamdelegate tostream-chain/jsonl/parserStream/parserWebStream. SupportsreviveranderrorIndicator.jsonl/stringer.js— delegates tostream-chain/jsonl/stringerStreamfor.asStreamandstream-chain/jsonl/stringerWebStreamfor.asWebStream. Configurableseparator,replacer,space,prefix,suffix,emptyValue.
jsonc/parser.js— thecharCodeAttokenizer ofparser.jsextended with//and/* */comments, trailing commas, and optionalwhitespace/ comment /commatokens (raw inner exportjsoncParser). Comments mirror strings — streamedstartComment/commentChunk/endCommentand packedcommentValue; the comment scan resumes across input chunks, so a comment of any length costs linear time (GHSA-hqr4-qq8f-hg3x). Options:streamWhitespace(default: true),streamComments(default: true),packComments(default: true),streamCommas(default: false — emit a valuelesscommatoken at every comma's position; the comma byte is already buffered, so emission needs no lookahead and is fully resumable). All standard parser options are supported.jsonc/stringer.js— fork ofstringer.jsthat passeswhitespaceand comment tokens through verbatim (useCommentValuesselects the packed form). OptionuseCommas(default: false) renders streamedcommatokens as,(a separator is still auto-inserted before a value when nocommatoken preceded it, so output stays valid even if commas were dropped upstream) —streamCommas+useCommasgive byte-faithful comma round-trips, incl. trailing commas. All standard stringer options are supported.jsonc/verifier.js— thecharCodeAtvalidator ofutils/verifier.jsextended to accept comments and trailing commas (raw inner exportjsoncVerifier). Reports error offset, line, and position for invalid JSONC.- Downstream compatibility: all existing filters, streamers, and utilities ignore unknown token types, so they work with JSONC parser output unmodified.
file/parser.js—parseFile(options)returnsgen(asyncBlockReader(options), jsonParser(options)). As the first stage in agen([…])pipeline, takes a path as the gen input value;exec.nextiterates the block-reading async generator (oneawaitper block, not per token), feeds each decoded chunk intojsonParser. Lives insrc/file/only — usesnode:fs/promises, so not incore/orweb/.file/verifier.js—verifyFile(path, options)is a standalone async function: constructspipe(asyncBlockReader, jsonVerifier)(path), drains it, propagates the verifier's{message, line, pos, offset}error on invalid input.file/stringer.js—stringerToFile(path, options)returnsgen(stringer(options), asyncBlockWriter(path, options)). The writer is a flushable that accumulates per-token text and writes fixed-size blocks viafh.write; itsfinal()writes the tail and closes theFileHandle. Requirespipe(...)to actually flush (and thereby close the file).file/jsonc/{parser,verifier,stringer}.js— JSONC variants; identical shapes wiring in the JSONC tokenizer/verifier/stringer.- The file-edge block primitives (
asyncBlockReader/asyncBlockWriter) and the generic drivers (pipe/drain) were incubated here, then moved to their canonical home instream-chain(stream-chain/utils/*); the file components import them from there now.stream-json/utils/{pipe,drain}remain as@deprecatedre-exports for back-compat (slated for removal in the next major); the local copies were deleted.
src/index.js ── src/parser.js, src/utils/emit.js
│
src/parser.js ── stream-chain (gen, flushable, many, none, asStream, fixUtf8Stream)
src/assembler.js ── stream-chain (none)
src/disassembler.js ── stream-chain (asStream)
src/stringer.js ── stream-chain (flushable, none, asStream)
src/emitter.js ── node:stream (Writable), src/web/emitter.js
src/web/emitter.js ── (global EventTarget, CustomEvent, WritableStream)
src/filters/filter-base.js ── stream-chain (many, isMany, getManyValues, combineManyMut, none, flushable)
src/filters/pick.js ── filter-base.js, with-parser.js
src/filters/replace.js ── stream-chain (none, isMany, getManyValues, combineManyMut, many), filter-base.js, with-parser.js
src/filters/ignore.js ── stream-chain (none), filter-base.js, with-parser.js
src/filters/filter.js ── filter-base.js, with-parser.js
src/streamers/stream-base.js ── stream-chain (none), assembler.js
src/streamers/stream-values.js ── stream-chain (none), stream-base.js, with-parser.js
src/streamers/stream-array.js ── stream-chain (none), stream-base.js, with-parser.js
src/streamers/stream-object.js ── stream-chain (none), stream-base.js, with-parser.js
src/utils/emit.js ── (standalone, no imports; Web variant: src/web/utils/emit.js)
src/web/utils/emit.js ── (global EventTarget, CustomEvent, WritableStream)
src/utils/with-parser.js ── stream-chain (asStream, gen), parser.js
src/utils/batch.js ── stream-chain (asStream), stream-chain/utils/batch
src/utils/verifier.js ── stream-chain (gen, flushable, none, asStream, fixUtf8Stream)
src/utils/flex-assembler.js ── stream-chain (none)
src/jsonl/parser.js ── stream-chain (gen, none, asStream, fixUtf8Stream, lines)
src/jsonl/stringer.js ── stream-chain/jsonl/stringerStream, stream-chain/jsonl/stringerWebStream
src/jsonc/parser.js ── stream-chain (gen, flushable, many, none, asStream, fixUtf8Stream)
src/jsonc/stringer.js ── stream-chain (flushable, none, asStream)
src/jsonc/verifier.js ── stream-chain (gen, flushable, none, asStream, fixUtf8Stream)
stream-json 3.x is ESM-only and runs on currently-supported Node.js (floor in package.json engines).
// Main API
import parserStream from 'stream-json'; // parser as Duplex stream (alias of parser.asStream)
import {parser} from 'stream-json'; // parser factory
// Core components
import Assembler from 'stream-json/assembler.js';
import {disassembler} from 'stream-json/disassembler.js';
import stringer from 'stream-json/stringer.js';
import emitter from 'stream-json/emitter.js';
// Filters
import {pick} from 'stream-json/filters/pick.js';
import {replace} from 'stream-json/filters/replace.js';
import {ignore} from 'stream-json/filters/ignore.js';
import {filter} from 'stream-json/filters/filter.js';
// Streamers
import {streamValues} from 'stream-json/streamers/stream-values.js';
import {streamArray} from 'stream-json/streamers/stream-array.js';
import {streamObject} from 'stream-json/streamers/stream-object.js';
// Utilities
import emit from 'stream-json/utils/emit.js';
import withParser from 'stream-json/utils/with-parser.js';
import batch from 'stream-json/utils/batch.js';
import verifier from 'stream-json/utils/verifier.js';
import FlexAssembler from 'stream-json/utils/flex-assembler.js';
// JSONL
import jsonlParser from 'stream-json/jsonl/parser.js';
import jsonlStringer from 'stream-json/jsonl/stringer.js';
// JSONC
import jsoncParser from 'stream-json/jsonc/parser.js';
import jsoncStringer from 'stream-json/jsonc/stringer.js';
import jsoncVerifier from 'stream-json/jsonc/verifier.js';- Framework: tape-six (
tape6) - Run all:
npm test(parallel workers viatape6 --flags FO) - Run single file:
node tests/test-<name>.js - Run with Bun:
npm run test:bun - Run sequential:
npm run test:proc - TypeScript check:
npm run ts-check - Lint:
npm run lint(Prettier check) - Lint fix:
npm run lint:fix(Prettier write)
Benchmarks use nano-benchmark. Run a benchmark by specifying its file:
npm run bench -- bench/<name>.js| File | What it measures |
|---|---|
bench/parser-jsonc.js |
Parser vs JSONC Parser on the same ~100 KB JSON array. Measures overhead of comment/trailing-comma support on plain JSON. |
bench/parser-jsonl.js |
parser({jsonStreaming: true}) + streamValues() vs jsonl/Parser. Shows native JSON.parse advantage for strict JSONL. |
bench/assembler-flex.js |
Assembler vs FlexAssembler (no rules) vs FlexAssembler (Map rules). Feeds pre-generated tokens via consume() — no stream overhead. |
All benchmarks generate synthetic data on the fly (~50–100 KB of mixed-type objects) to isolate component performance from I/O.