json-marshal
v0.0.5
Published
JSON serializer that can stringify and parse any data type.
Maintainers
Readme
npm install --save-prod json-marshal- Supports circular references.
- Supports stable serialization.
- Zero serialization redundancy — never serializes the same object twice.
- Zero dependencies.
- Can serialize any data type via adapters.
- Supports many data types out of the box.
- Extremely fast.
- Only 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);
// ⮕ Uint8ArrayJSON 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] });
// ⮕ DateOr 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, 199] are reserved for internal use and 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