Skip to content
LogoLogo

@portable/jsonl

Decodes and encodes JSON Lines (.jsonl, also called NDJSON or newline-delimited JSON) streams. Runs in browsers, Cloudflare Workers, Deno, Bun and Node 24+.

Replaces ndjson and the jsonl modules of stream-json.

npm install @portable/jsonl
import * as Jsonl from '@portable/jsonl';
import * as Streams from '@portable/streams';
 
const response = await fetch('https://example.com/events.ndjson');
const events = response.body!.pipeThrough(new Jsonl.DecoderStream());
for await (const event of Streams.toAsyncIterable(events)) {
  console.log(event);
}

DecoderStream is a TransformStream<Uint8Array | string, unknown> that parses each line with JSON.parse. Lines may end in LF or CRLF, blank and whitespace-only lines are skipped, and a last line with no newline is decoded too. Byte chunks are decoded as UTF-8, so a multi-byte character split across chunks is decoded correctly.

EncoderStream is a TransformStream<unknown, Uint8Array> that writes each value as JSON.stringify(value) followed by \n, in UTF-8. A value that JSON.stringify can't encode (undefined, a function, a symbol, a BigInt or a circular structure) fails the stream with a TypeError. Its output can be a Response body as it is:

const body = Streams.fromIterable(rows).pipeThrough(
  new Jsonl.EncoderStream(),
);
return new Response(body, {
  headers: { 'content-type': 'application/x-ndjson' },
});

Options

DecoderStream takes invalid, maxLength and signal. EncoderStream takes signal.

OptionDefaultDescription
invalid'error'A line that isn't valid JSON: 'error' fails the stream with InvalidInputError, whose message gives the line number, and 'skip' drops the line.
maxLengthInfinityLongest line, in UTF-16 code units, line ending excluded. As soon as a line grows past the limit, the stream fails with Jsonl.MaxLengthError, which stores the limit in maxLength. Set it when the input comes from outside the program.
signalnoneAborting errors the stream with signal.reason and cancels the source piped into it.

Why use this over ndjson and stream-json

ndjson is a pair of Node object streams. DecoderStream and EncoderStream are TransformStreams, so they read a fetch body and write a Response body in any JavaScript runtime, and the package has no dependencies outside @portable/*.

  • Decoding 20,000 rows ran about 1.1× faster than ndjson, and encoding them about 1.5× faster. stream-json's jsonl/Stringer encoded them about 1.3× faster than EncoderStream.
  • A null line is emitted as null. In ndjson a null line ends the stream, because Node object streams treat null as the end (ndjson #6).
  • EncoderStream ends every line with \n. ndjson uses os.EOL, so on Windows it writes \r\n.
  • A circular value fails the stream with a TypeError. ndjson writes "[Circular ~]" in its place, which changes the data.
  • A whitespace-only line is skipped, where ndjson reports it as invalid.
  • stream-json's jsonl/Parser emits { key, value } objects, and DecoderStream emits each value on its own.
  • stream-json's jsonl/Parser can replace a bad line with another value or pass its raw text on, and DecoderStream can't. To do that, split with Lines.DecoderStream from @portable/lines and call JSON.parse yourself.
@portable/jsonlndjsonstream-jsonJSON.parse or JSON.stringify
Decode 20,000 rows38.634.327.844.5
Encode 20,000 rows29.219.038.779.3

Operations per second from pnpm bench, higher is better, in Node 24.22.0, on Intel(R) Xeon(R) Processor @ 2.10GHz (4 cores). A dash means the library has no equivalent.

@portable/jsonlndjsonstream-jsonJSON.parse or JSON.stringify
Minified and gzipped1.2 kB13 kB3.5 kBbuilt in
Runtime dependencies391–
Node built-ins usednonebuffer, events, os, string_decoder, utilstream, string_decodernone
TypeScript typesincluded@types/ndjsonincluded–
Version compared–2.0.03.7.0–

Sizes cover the imports in the usage example, or the equivalent imports of each library, bundled for the browser by scripts/bundle-size.ts. Runtime dependencies count every package installed with the published package. A library that uses Node built-ins needs polyfills in a browser and the nodejs_compat flag in Cloudflare Workers.