json-marshal
v0.0.4
Published
JSON serializer that can stringify and parse any data type.
Downloads
212
Maintainers
Readme
npm install --save-prod json-marshal
- Supports circular references.
- Serialization redundancy is zero: never serializes the same object twice.
- Supports stable serialization.
- Can serialize anything via adapters.
- Supports many data types out-of-the-box:
undefined
NaN
andInfinity
BigInt
Date
RegExp
Map
Set
- Typed arrays,
DataView
andArrayBuffer
- Errors, including
DOMException
- It is very fast.
- 2 kB gzipped.
import JSONMarshal from 'json-marshal';
const json = JSONMarshal.stringify({ hello: /Old/g });
// ⮕ '{"hello":[102,["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
functions 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":[102,["Old","g"]]}'
parse(json, options);
// ⮕ { hello: /Old/g }
Or create a custom serializer:
import { createSerializer } from 'json-marshal';
import arrayBufferAdapter from 'json-marshal/adapter/array-buffer';
const serializer = createSerializer({ adapters: [arrayBufferAdapter()] });
const json = serializer.stringify(new TextEncoder().encode('aaa bbb ccc'));
// ⮕ '[105,["YWFhIGJiYiBjY2M=",2]]'
serializer.parse(json);
// ⮕ Uint8Array
JSON Marshal supports circular references:
const obj = {};
obj.circularReference = obj;
serialize(obj);
// ⮕ '{"circularReference":[0,0]}'
Out-of-the-box undefined
, NaN
, Infinity
, and BigInt
are stringified:
stringify(undefined);
// ⮕ '[1]'
stringify(1_000_000n);
// ⮕ '[5,"1000000"]'
By default, object properties with undefined
values aren't serialized. Force undefined
properties serialization with
isUndefinedPropertyValuesPreserved
option:
const obj = { hello: undefined };
stringify(obj);
// ⮕ '{}'
stringify(obj, { isUndefinedPropertyValuesPreserved: 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 JSONMarshal from 'json-marshal';
const user = { name: 'Bill' };
const employees = [user, user, user];
JSON.stringify(employees);
// ⮕ '[{"name":"Bill"},{"name":"Bill"},{"name":"Bill"}]'
JSONMarshal.stringify(employees);
// ⮕ [{"name":"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:
import { stringify } from 'json-marshal';
stringify({ kill: 'Bill', hello: 'Greg' });
// ⮕ '{"kill":"Bill","hello":"Greg"}'
Provide
isStable
option to sort keys in alphabetical order:
stringify({ kill: 'Bill', hello: 'Greg' }, { isStable: true });
// ⮕ '{"hello":"Greg","kill":"Bill"}'
Serialization adapters
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()] });
// ⮕ '[105,["AAAAAAAAAAAAAA==",0]]'
When deserializing, the same adapters must be provided, or an error would be thrown:
import { parse } from 'json-marshal';
parse(json);
// ❌ Error: Adapter not found for tag: 105
parse(json, { adapters: [arrayBufferAdapter()] });
// ⮕ ArrayBuffer(10)
Built-in adapters
Built-in adapters can be imported from json-marshal/adapter/*
:
import arrayBufferAdapter from 'json-marshal/adapter/array-buffer';
stringify(new ArrayBuffer(10), { adapters: [arrayBufferAdapter()] });
json-marshal/adapter/array-buffer
Serializes typed arrays,
DataView
and ArrayBuffer
instances as Base64-encoded string.
json-marshal/adapter/date
Serializes Date
instances.
json-marshal/adapter/error
Serializes DOMException
,
Error
,
EvalError
,
RangeError
,
ReferenceError
,
SyntaxError
,
TypeError
, and
URIError
.
json-marshal/adapter/map
Serializes Map
instances. If isStable
option is provided, Map
keys are sorted in alphabetical order.
json-marshal/adapter/regexp
Serializes RegExp
instances.
json-marshal/adapter/set
Serializes Set
instances. If isStable
option is provided, Set
items are sorted in alphabetical order.
Authoring a serialization adapter
Create a custom adapter for your object type. For example, let's create a Date
adapter:
import { SerializationAdapter } from 'json-marshal';
const dateAdapter: SerializationAdapter<Date, string> = {
tag: 1111,
canPack(value) {
return value instanceof Date;
},
pack(value, options) {
return value.toISOString();
},
unpack(payload, options) {
return new Date(payload);
},
};
Here's how to use the adapter:
import { stringify, parse } from 'json-marshal';
const json = stringify(new Date(), { adapters: [dateAdapter] });
// ⮕ '[1111,"2025-03-30T13:13:59.135Z"]'
parse(json, { adapters: [dateAdapter] });
// ⮕ Date
Or create a custom serializer:
import { createSerializer } from 'json-marshal';
const serializer = createSerializer({ adapters: [dateAdapter] });
const json = serializer.stringify(new Date());
// ⮕ '[1111,"2025-03-30T13:13:59.135Z"]'
serializer.parse(json);
// ⮕ Date { 2025-03-30T13:13:59.135Z }
tag
is an integer
that uniquely identifies the adapter during serialization and deserialization.
[!IMPORTANT]
Tags in range [0, 100) are reserved for internal use. Tags in range [100, 200) are used by built-in adapters.
During serialization, each value is passed to the
canPack
method which should return true
if an adapter can pack a value as a serializable payload.
Then the pack
method converts the value into a serializable payload. The payload returned from the
pack
method is dehydrated before stringification: circular and repeated references are encoded.
During deserialization,
unpack
method
receives the dehydrated payload and must return the shallow value to which references may point. Note that since payload
isn't hydrated at this stage, it may still contain encoded refs.
After payload is unpacked,
hydrate
method is called and it receives the value returned by unpack
and hydrated payload.
Separation of unpack
and hydrate
allows to restore cyclic references in an arbitrary object.
Let's create a Set
adapter to demonstrate how to use hydrate
:
import { SerializationAdapter } from 'json-marshal';
const setAdapter: SerializationAdapter<Set<any>, any[]> = {
tag: 2222,
canPack(value) {
return value instanceof Set;
},
pack(value, options) {
return Array.from(value);
},
unpack(payload, options) {
// Return an empty Set, we'll populate it with hydrated items later
return new Set();
},
hydrate(value, payload, options) {
// Add hydrated items to the Set
for (const item of payload) {
value.add(item);
}
},
};
Now we can stringify and parse Set
instances using setAdapter
:
import { stringify, parse } from 'json-marshal';
const json = stringify(new Set(['aaa', 'bbb']), { adapters: [setAdapter] });
// ⮕ '[2222,["aaa","bbb"]]'
parse(json, { adapters: [setAdapter] });
// ⮕ Set { 'aaa', 'bbb' }
Let's stringify a Set
that contains a self-reference:
import { stringify, parse } from 'json-marshal';
const obj = new Set();
obj.add(obj);
const json = stringify(obj, { adapters: [setAdapter] });
// ⮕ '[2222,[[0,0]]]'
parse(json, { adapters: [setAdapter] });
// ⮕ Set { <self_reference> }
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 v23.1.0.
To reproduce the performance test suite results, clone this repo and run:
npm ci
npm run build
npm run perf