npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

json-marshal

v0.0.2

Published

JSON serializer that can stringify and parse any data type.

Downloads

100

Readme

JSON Marshal

JSON serializer that can stringify and parse any data type.

npm install --save-prod json-marshal
import JSONMarshal from 'json-marshal';

const json = JSONMarshal.stringify({ hello: /Old/g });
// ⮕ '{"hello":[7,"Old","g"]}'

JSONMarshal.parse(json)
// ⮕ { hello: /Old/g }

Overview

The default export provides a serializer that can be used as a drop-in replacement for JSON:

import JSONMarshal from 'json-marshal';

JSONMarshal.stringify('Hello');
// ⮕ '"Hello"'

Import parse and stringify function separately to have a fine-grained control over serialization:

import { stringify, parse, SerializationOptions } from 'json-marshal';
import regexpAdapter from 'json-marshal/adapter/regexp';

const options: SerializationOptions = {
  adapters: [regexpAdapter()]
};

const json = serialize({ hello: /Old/g }, options);
// ⮕ '{"hello":[7,"Old","g"]}'

parse(json, options);
// ⮕ { hello: /Old/g }

Or create a custom serializer:

import { createSerializer } from 'json-marshal';

const serializer = createSerializer({ adapters: [regexpAdapter()] });

serializer.stringify(/Old/g);
// ⮕ '[7,"Old","g"]'

JSON Marshal supports circular references:

const gunslinger = {};

// Circular reference
gunslinger.bill = gunslinger;

serialize(hello);
// ⮕ '{"bill":[0,0]}'

Out-of-the-box undefined, NaN, Infinity, and BigInt can be stringified:

stringify(undefined);
// ⮕ '[1]'

stringify(1_000_000n);
// ⮕ '[5,"1000000"]'

By default, object properties with undefined values aren't serialized. Override this with undefinedPropertyValuesPreserved option:

const gunslinger = { hello: undefined };

stringify(gunslinger);
// ⮕ '{}'

stringify(gunslinger, { undefinedPropertyValuesPreserved: true });
// ⮕ '{"hello":[1]}'

All objects are always serialized only once and then referenced if needed, so no excessive serialization is performed. This results in a smaller output and faster serialization/deserialization times in comparison to peers:

import { stringify } from 'json-marshal';

const gunslinger = { hello: 'bill' };

const gang = [gunslinger, gunslinger, gunslinger];

JSON.stringify(gang);
// ⮕ '[{"hello":"bill"},{"hello":"bill"},{"hello":"bill"}]'

stringify(gang);
// ⮕ [{"hello":"bill"},[0,1],[0,1]]

By default, object property keys appear in the serialized string in the same order they were added to the object:

stringify({ kill: 'Bill', hello: 'Greg' });
// ⮕ '{"kill":"Bill","hello":"Greg"}'

Provide stable option to sort keys in alphabetical order:

stringify({ kill: 'Bill', hello: 'Greg' }, { stable: true });
// ⮕ '{"hello":"Greg","kill":"Bill"}'

Serialization adapters

By default, only enumerable object properties are stringified:

stringify({ hello: 'Bob' });
// ⮕ '{"hello":"Bob"}'

stringify(new ArrayBuffer(10));
// ⮕ '{}'

Provide a serialization adapter that supports the required object type to enhance serialization:

import { stringify } from 'json-marshal';
import arrayBufferAdapter from 'json-marshal/adapter/array-buffer';

const json = stringify(new ArrayBuffer(10), { adapters: [arrayBufferAdapter()] });
// ⮕ '[23,"AAAAAAAAAAAAAA=="]'

When deserializing, the same adapters must be provided, or an error would be thrown:

import { parse } from 'json-marshal';

parse(json);
// ❌ Error: Unrecognized tag: 23

parse(json, { adapters: [arrayBufferAdapter()] });
// ⮕ ArrayBuffer(10)

Built-in adapters

Built-in adapters can be imported as json-marshal/adapter/*:

import arrayBufferAdapter from 'json-marshal/adapter/array-buffer';

stringify(new ArrayBuffer(10), { adapters: [arrayBufferAdapter()] });

Serializes typed arrays, DataView and ArrayBuffer instances as Base64-encoded string.

Serializes Date instances.

Serializes DOMException, Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, and URIError.

Serializes Map instances. If stable option is provided, Map keys are sorted in alphabetical order.

Serializes RegExp instances.

Serializes Set instances. If stable option is provided, Set items are sorted in alphabetical order.

Authoring a serialization adapter

You can create custom adapters for your object types. For example, let's create a Date adapter:

import { SerializationAdapter } from 'json-marshal';

const DATE_TAG = 222;

const dateAdapter: SerializationAdapter = {

  getTag: (value, options) =>
    value instanceof Date ? DATE_TAG : undefined,

  getPayload: (tag, value, options) =>
    value.getTime(),

  getValue: (tag, dehydratedPayload, options) =>
    tag === DATE_TAG ? new Date(dehydratedPayload) : undefined,
};

During serialization, each object is passed to the getTag method. If must return the unique tag (a positive integer) of the value type, or undefined if the adapter doesn't recognize the type of the given value.

Then the getPayload method is used to convert the value into a serializable form. The payload returned from the getPayload method is stringified. During stringification, payloads are dehydrated: circular references and reused references are replaced with tags. For example, the tag that references the second object during the depth-first traversal looks kile this: [0,1].

During deserialization, getValue method receives the dehydrated payload along with its tag and must return a deserialized value, or undefined if deserialization isn't supported for the given tag. If getValue returns a non-undefined value, a hydrateValue method is called. It receives a value created by getValue and the hydrated payload that can be used to enrich the original value.

For example, if you're deserializing a Set instance, then new Set() must be returned from the getValue, and in hydrateValue items from the hydrated payload should be added to the set. This approach allows to hydrate cyclic references in an arbitrary object. If value hydration isn't required (like in the example with Date serialization), hydrateValue method can be omitted.

Let's use our dateAdapter:

import { stringify, parse } from 'json-marshal';

const json = stringify({ today: new Date() }, { adapters: [dateAdapter] });
// ⮕ '{"today":[222,1716291110044]}'

parse(json, { adapters: [dateAdapter] });
// ⮕ { today: Date }

Return DISCARDED from the getPayload method to exclude the provided value from being stringified. For example, lets write an adapter that serializes runtime-wide symbols and discards local symbols.

import { DISCARDED, stringify, SerializationAdapter } from 'json-marshal';

const SYMBOL_TAG = 333;

const symbolAdapter: SerializationAdapter = {

  getTag: (value, options) =>
    typeof value === 'symbol' ? SYMBOL_TAG : undefined,

  // 🟡 Only runtime-wide symbols are serialized
  getPayload: (tag, value, options) =>
    Symbol.for(value.description) === value ? value.description : DISCARDED,

  getValue: (tag, dehydratedPayload, options) =>
    tag === SYMBOL_TAG ? Symbol.for(dehydratedPayload) : undefined,
};

Runtime-wide symbols can now be serialized with symbolAdapter:

stringify([Symbol.for('hello')], { adapters: [symbolAdapter] });
// ⮕ '[[333,"hello"]]'

// 🟡 Local symbol is discarded in serialized data
stringify([Symbol('goodbye')], { adapters: [symbolAdapter] });
// ⮕ '[]'

Performance

The chart below showcases the performance comparison of JSON Marshal and its peers, in terms of thousands of operations per second (greater is better).

Tests were conducted using TooFast on Apple M1 with Node.js v22.2.0.

To reproduce the performance test suite results, clone this repo and run:

npm ci
npm run build
npm run perf