Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

@discoveryjs/json-ext

discoveryjs40.2mMIT0.6.3TypeScript support: included

A set of utilities that extend the use of JSON

json, utils, stream, async, promise, stringify, info

readme

json-ext

NPM version Build Status Coverage Status NPM Downloads

A set of utilities designed to extend JSON's capabilities, especially for handling large JSON data (over 100MB) efficiently:

Key Features

  • Optimized to handle large JSON data with minimal resource usage (see benchmarks)
  • Works seamlessly with browsers, Node.js, Deno, and Bun
  • Supports both Node.js and Web streams
  • Available in both ESM and CommonJS
  • TypeScript typings included
  • No external dependencies
  • Compact size: 9.4Kb (minified), 3.8Kb (min+gzip)

Why json-ext?

  • Handles large JSON files: Overcomes the limitations of V8 for strings larger than ~500MB, enabling the processing of huge JSON data.
  • Prevents main thread blocking: Distributes parsing and stringifying over time, ensuring the main thread remains responsive during heavy JSON operations.
  • Reduces memory usage: Traditional JSON.parse() and JSON.stringify() require loading entire data into memory, leading to high memory consumption and increased garbage collection pressure. parseChunked() and stringifyChunked() process data incrementally, optimizing memory usage.
  • Size estimation: stringifyInfo() allows estimating the size of resulting JSON before generating it, enabling better decision-making for JSON generation strategies.

Install

npm install @discoveryjs/json-ext

API

parseChunked()

Functions like JSON.parse(), iterating over chunks to reconstruct the result object, and returns a Promise.

Note: reviver parameter is not supported yet.

function parseChunked(input: Iterable<Chunk> | AsyncIterable<Chunk>): Promise<any>;
function parseChunked(input: () => (Iterable<Chunk> | AsyncIterable<Chunk>)): Promise<any>;

type Chunk = string | Buffer | Uint8Array;

Benchmark

Usage:

import { parseChunked } from '@discoveryjs/json-ext';

const data = await parseChunked(chunkEmitter);

Parameter chunkEmitter can be an iterable or async iterable that iterates over chunks, or a function returning such a value. A chunk can be a string, Uint8Array, or Node.js Buffer.

Examples:

  • Generator:
      parseChunked(function*() {
          yield '{ "hello":';
          yield Buffer.from(' "wor'); // Node.js only
          yield new TextEncoder().encode('ld" }'); // returns Uint8Array
      });
  • Async generator:
      parseChunked(async function*() {
          for await (const chunk of someAsyncSource) {
              yield chunk;
          }
      });
  • Array:
      parseChunked(['{ "hello":', ' "world"}'])
  • Function returning iterable:
      parseChunked(() => ['{ "hello":', ' "world"}'])
  • Node.js Readable stream:

      import fs from 'node:fs';
    
      parseChunked(fs.createReadStream('path/to/file.json'))
  • Web stream (e.g., using fetch()):

    Note: Iterability for Web streams was added later in the Web platform, not all environments support it. Consider using parseFromWebStream() for broader compatibility.

      const response = await fetch('https://example.com/data.json');
      const data = await parseChunked(response.body); // body is ReadableStream

stringifyChunked()

Functions like JSON.stringify(), but returns a generator yielding strings instead of a single string.

Note: Returns "null" when JSON.stringify() returns undefined (since a chunk cannot be undefined).

function stringifyChunked(value: any, replacer?: Replacer, space?: Space): Generator<string, void, unknown>;
function stringifyChunked(value: any, options: StringifyOptions): Generator<string, void, unknown>;

type Replacer =
    | ((this: any, key: string, value: any) => any)
    | (string | number)[]
    | null;
type Space = string | number | null;
type StringifyOptions = {
    replacer?: Replacer;
    space?: Space;
    highWaterMark?: number;
};

Benchmark

Usage:

  • Getting an array of chunks:
      const chunks = [...stringifyChunked(data)];
  • Iterating over chunks:
      for (const chunk of stringifyChunked(data)) {
          console.log(chunk);
      }
  • Specifying the minimum size of a chunk with highWaterMark option:

      const data = [1, "hello world", 42];
    
      console.log([...stringifyChunked(data)]); // default 16kB
      // ['[1,"hello world",42]']
    
      console.log([...stringifyChunked(data, { highWaterMark: 16 })]);
      // ['[1,"hello world"', ',42]']
    
      console.log([...stringifyChunked(data, { highWaterMark: 1 })]);
      // ['[1', ',"hello world"', ',42', ']']
  • Streaming into a stream with a Promise (modern Node.js):

      import { pipeline } from 'node:stream/promises';
      import fs from 'node:fs';
    
      await pipeline(
          stringifyChunked(data),
          fs.createWriteStream('path/to/file.json')
      );
  • Wrapping into a Promise streaming into a stream (legacy Node.js):

      import { Readable } from 'node:stream';
    
      new Promise((resolve, reject) => {
          Readable.from(stringifyChunked(data))
              .on('error', reject)
              .pipe(stream)
              .on('error', reject)
              .on('finish', resolve);
      });
  • Writing into a file synchronously:

    Note: Slower than JSON.stringify() but uses much less heap space and has no limitation on string length `js import fs from 'node:fs';

    const fd = fs.openSync('output.json', 'w');

    for (const chunk of stringifyChunked(data)) {

      fs.writeFileSync(fd, chunk);

    }

    fs.closeSync(fd); `

  • Using with fetch (JSON streaming):

    Note: This feature has limited support in browsers, see Streaming requests with the fetch API

    Note: ReadableStream.from() has limited support in browsers, use createStringifyWebStream() instead.

      fetch('http://example.com', {
          method: 'POST',
          duplex: 'half',
          body: ReadableStream.from(stringifyChunked(data))
      });
  • Wrapping into ReadableStream:

    Note: Use ReadableStream.from() or createStringifyWebStream() when no extra logic is needed `js new ReadableStream({

      start() {
          this.generator = stringifyChunked(data);
      },
      pull(controller) {
          const { value, done } = this.generator.next();
          if (done) {
              controller.close();
          } else {
              controller.enqueue(value);
          }
      },
      cancel() {
          this.generator = null;
      }

    }); `

stringifyInfo()

export function stringifyInfo(value: any, replacer?: Replacer, space?: Space): StringifyInfoResult;
export function stringifyInfo(value: any, options?: StringifyInfoOptions): StringifyInfoResult;

type StringifyInfoOptions = {
    replacer?: Replacer;
    space?: Space;
    continueOnCircular?: boolean;
}
type StringifyInfoResult = {
    bytes: number;      // size of JSON in bytes
    spaceBytes: number; // size of white spaces in bytes (when space option used)
    circular: object[]; // list of circular references
};

Functions like JSON.stringify(), but returns an object with the expected overall size of the stringify operation and a list of circular references.

Example:

import { stringifyInfo } from '@discoveryjs/json-ext';

console.log(stringifyInfo({ test: true }, null, 4));
// {
//   bytes: 20,     // Buffer.byteLength('{\n    "test": true\n}')
//   spaceBytes: 7,
//   circular: []    
// }

Options

continueOnCircular

Type: Boolean
Default: false

Determines whether to continue collecting info for a value when a circular reference is found. Setting this option to true allows finding all circular references.

parseFromWebStream()

A helper function to consume JSON from a Web Stream. You can use parseChunked(stream) instead, but @@asyncIterator on ReadableStream has limited support in browsers (see ReadableStream compatibility table).

import { parseFromWebStream } from '@discoveryjs/json-ext';

const data = await parseFromWebStream(readableStream);
// equivalent to (when ReadableStream[@@asyncIterator] is supported):
// await parseChunked(readableStream);

createStringifyWebStream()

A helper function to convert stringifyChunked() into a ReadableStream (Web Stream). You can use ReadableStream.from() instead, but this method has limited support in browsers (see ReadableStream.from() compatibility table).

import { createStringifyWebStream } from '@discoveryjs/json-ext';

createStringifyWebStream({ test: true });
// equivalent to (when ReadableStream.from() is supported):
// ReadableStream.from(stringifyChunked({ test: true }))

License

MIT

changelog

0.6.3 (2024-10-24)

  • Fixed an issue with types in the exports of package.json that introduced in version 0.6.2

0.6.2 (2024-10-18)

  • Added spaceBytes field to stringifyInfo() result, which indicates the number of bytes used for white spaces. This allows for estimating size of JSON.stringify() result with and without formatting (when space option is used) in a single pass instead of two
  • Fixed stringifyInfo() to correctly accept the space parameter from options, i.e. stringifyInfo(data, { space: 2 })

0.6.1 (2024-08-06)

  • Enhanced the performance of stringifyChunked() by 1.5-3x
  • Enhanced the performance of stringifyInfo() by 1.5-5x
  • Fixed parseFromWebStream() to ensure that the lock on the reader is properly released

0.6.0 (2024-07-02)

  • Added stringifyChunked() as a generator function (as a replacer for stringifyStream())
  • Added createStringifyWebStream() function
  • Added parseFromWebStream() function
  • Changed parseChunked() to accept an iterable or async iterable that iterates over string, Buffer, or TypedArray elements
  • Removed stringifyStream(), use Readable.from(stringifyChunked()) instead
  • Fixed conformance stringifyChunked() with JSON.stringify() when replacer a list of keys and a key refer to an entry in a prototype chain
  • stringifyInfo():
    • Aligned API with stringifyChunked by accepting options as the second parameter. Now supports:
      • stringifyInfo(value, replacer?, space?)
      • stringifyInfo(value, options?)
    • Renamed minLength field into bytes in functions result
    • Removed the async option
    • The function result no longer contains the async and duplicate fields
    • Fixed conformance with JSON.stringify() when replacer a list of keys and a key refer to an entry in a prototype chain
  • Discontinued exposing the version attribute
  • Converted to Dual Package, i.e. ESM and CommonJS support

0.5.7 (2022-03-09)

  • Fixed adding entire package.json content to a bundle when target is a browser

0.5.6 (2021-11-30)

  • Fixed stringifyStream() hang when last element in a stream takes a long time to process (#9, @kbrownlees)

0.5.5 (2021-09-14)

  • Added missed TypeScript typings file into the npm package

0.5.4 (2021-09-14)

  • Added TypeScript typings (#7, @lexich)

0.5.3 (2021-05-13)

  • Fixed stringifyStream() and stringifyInfo() to work properly when replacer is an allowlist
  • parseChunked()
    • Fixed wrong parse error when chunks are splitted on a whitespace inside an object or array (#6, @alexei-vedder)
    • Fixed corner cases when wrong placed or missed comma doesn't cause to parsing failure

0.5.2 (2020-12-26)

  • Fixed RangeError: Maximum call stack size exceeded in parseChunked() on very long arrays (corner case)

0.5.1 (2020-12-18)

  • Fixed parseChunked() crash when input has trailing whitespaces (#4, @smelukov)

0.5.0 (2020-12-05)

  • Added support for Node.js 10

0.4.0 (2020-12-04)

  • Added parseChunked() method
  • Fixed stringifyInfo() to not throw when meet unknown value type

0.3.2 (2020-10-26)

  • Added missed file for build purposes

0.3.1 (2020-10-26)

  • Changed build setup to allow building by any bundler that supports browser property in package.json
  • Exposed version

0.3.0 (2020-09-28)

  • Renamed info() method into stringifyInfo()
  • Fixed lib's distribution setup

0.2.0 (2020-09-28)

  • Added dist version to package (dist/json-ext.js and dist/json-ext.min.js)

0.1.1 (2020-09-08)

  • Fixed main entry point

0.1.0 (2020-09-08)

  • Initial release