true-json

TrueJSON: respectful JSON serialization & deserialization for JavaScript

Usage no npm install needed!

<script type="module">
  import trueJson from 'https://cdn.skypack.dev/true-json';
</script>

README

TrueJSON

Respectful JSON serialization & deserialization for JavaScript

License Contributor Covenant

Coverage statements Coverage branches Coverage functions Coverage lines

Table of contents

What's TrueJSON?

TrueJSON is a JSON serialization and deserialization library for JavaScript and TypeScript. It helps with the serialization and deserialization of complex types such as Date, Set, Map, and many others.

What's wrong with JSON.stringify() and JSON.parse()?

Imagine the following JavaScript object:

const originalObject = {
    date: new Date(),
    set: new Set([1, 2, 3])
};

const serializedObject = JSON.stringify(originalObject);

const deserializedObject = JSON.parse(serializedObject);

If you check the value of the serializedObject variable, you'll see the following JSON structure:

{
    "date": "1970-01-01T00:00:00.000Z",
    "set": {}
}

As you can see, your set elements haven't been serialized as you would expect. Moreover, if you check the value of the deserializedObject variable, you'll see the following object:

Deserialized object using native JSON (Google Chrome console)

Now the date property is a string, and the set property is an empty object, which, probably, isn't the desired behaviour.

TrueJSON to the rescue

Using TrueJSON, you can create a JsonConverter that knows how to serialize and deserialize your object without loosing any information. This can be done by using JSON adapters, which are components that know how to adapt some data types to jsonable values. Let's see an example:

import {JsonConverter, JsonAdapters} from 'true-json';

// Create a converter using adapters to describe your object's type
const customJsonConverter = new JsonConverter(JsonAdapters.object({
    date: JsonAdapters.isoDate(),
    set: JsonAdapters.set()
}));

const originalObject = {
    date: new Date(),
    set: new Set([1, 2, 3])
};

const serializedObject = customJsonConverter.stringify(originalObject);

const deserializedObject = customJsonConverter.parse(serializedObject);

If you check the value of the serializedObject variable, now you'll see the following JSON structure:

{
    "date": "1970-01-01T00:00:00.000Z",
    "set": [
        1,
        2,
        3
    ]
}

As you can see, now your Set has been serialized as a JSON array, preserving its values in the JSON structure. In addition, if you check the value of the deserializedObject variable, you'll see the following object:

Deserialized object using TrueJSON (Google Chrome console)

As you can see, both the date property and the set property have been deserialized to Date and Set objects respectively.

In addition, TrueJSON will perform some validations (i.e. type checks) before serializing or deserializing, throwing errors if the received input doesn't match the expected structure.

Features

Installation

Using NPM (module)

Install the latest stable version:

npm install --save true-json

Then you can import TrueJSON objects in your modules:

import {JsonConverter, JsonAdapters} from 'true-json';

Using <script> tag (standalone)

You can download the latest version from here. Then, you can use it as any other JavaScript file:


<script src="true-json.js"></script>

Or, if you prefer, you can use any of the following CDN repositories:

<!-- Unpkg -->
<script src="https://unpkg.com/true-json@1.0.0"></script>

<!-- JsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/true-json@1.0.0"></script>

The script will create a global TrueJSON object, which contains all the exported objects.

Basic usage

Using import (module)

import {JsonConverter, JsonAdapters} from 'true-json';

const user = {
    name: 'John Doe',
    birthDate: new Date('1970-01-01'),
    bestScoreByGame: new Map([
        ['Minesweeper', 118],
        ['Donkey Kong', 35500],
        ['Super Mario Bros.', 183250],
    ])
};

const userJsonConverter = new JsonConverter(JsonAdapters.object({
    birthDate: JsonAdapters.isoDate(),
    bestScoreByGame: JsonAdapters.mapAsRecord()
}));

const userAsJson = userJsonConverter.stringify(user);

console.log(userAsJson);

Using TrueJSON object (standalone)

You can access any object just by doing TrueJSON.[ObjectName]:

const user = {
    name: 'John Doe',
    birthDate: new Date('1970-01-01'),
    bestScoreByGame: new Map([
        ['Minesweeper', 118],
        ['Donkey Kong', 35500],
        ['Super Mario Bros.', 183250],
    ])
};

const userJsonConverter = new TrueJSON.JsonConverter(TrueJSON.JsonAdapters.object({
    birthDate: TrueJSON.JsonAdapters.isoDate(),
    bestScoreByGame: TrueJSON.JsonAdapters.mapAsRecord()
}));

const userAsJson = userJsonConverter.stringify(user);

console.log(userAsJson);

You can also use ES6 destructuring assignment in order to imitate module imports:

const {JsonConverter, JsonAdapters} = TrueJSON;

const user = {
    name: 'John Doe',
    birthDate: new Date('1970-01-01'),
    bestScoreByGame: new Map([
        ['Minesweeper', 118],
        ['Donkey Kong', 35500],
        ['Super Mario Bros.', 183250],
    ])
};

const userJsonConverter = new JsonConverter(JsonAdapters.object({
    birthDate: JsonAdapters.isoDate(),
    bestScoreByGame: JsonAdapters.mapAsRecord()
}));

const userAsJson = userJsonConverter.stringify(user);

console.log(userAsJson);

Using JSON5 or other JSON alternatives

You can configure TrueJSON's JsonConverter to use any custom JSON implementation, such as JSON5. The only requirement is that the custom JSON implementation should have the same parse() and stringify() methods as the standard one.

Let's see an example using json5's NPM package:

import json5 from 'json5';
import {JsonConverter, JsonAdapters} from 'true-json';

const user = {
    name: 'John Doe',
    birthDate: new Date('1970-01-01'),
    bestScoreByGame: new Map([
        ['Minesweeper', 118],
        ['Donkey Kong', 35500],
        ['Super Mario Bros.', 183250],
    ])
};

const userJsonAdapter = JsonAdapters.object({
    birthDate: JsonAdapters.isoDate(),
    bestScoreByGame: JsonAdapters.mapAsRecord()
});

// The second argument of JsonConverter's constructor
// allows you to pass a custom JSON implementation.
// When no value is passed, the standard JSON object is used.
const userJsonConverter = new JsonConverter(userJsonAdapter, json5);

const userAsJson = userJsonConverter.stringify(user);

console.log(userAsJson);

Built-in adapters

In this section, we will cover the build-in adapters that TrueJSON provides to you.

isoDate()

This adapter converts any Date object into his ISO textual representation (see Date.prototype.toISOString() docs) and vice versa:

const adapter = JsonAdapters.isoDate();

console.log(adapter.adaptToJson(new Date(0)));

console.log(adapter.recoverFromJson('1970-01-01T00:00:00.000Z'));

Output:

"1970-01-01T00:00:00.000Z"

Date { Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time) }

dateTimestamp()

This adapter converts any Date object into his number representation in milliseconds (see Date.prototype.getTime() docs) and vice versa:

const adapter = JsonAdapters.dateTimestamp();

console.log(adapter.adaptToJson(new Date(0)));

console.log(adapter.recoverFromJson(0));
0

Date { Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time) }

array(elementAdapter)

Using this adapter you can specify how the elements of the array should be adapted:

const adapter = JsonAdapters.array(JsonAdapters.dateTimestamp());

console.log(adapter.adaptToJson([
    new Date(0),
    new Date(1620458583563)
]));

console.log(adapter.recoverFromJson([
    0,
    1620458583563
]));

Output:

[0, 1620458583563]

[
    Date { Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time) },
    Date { Sat May 08 2021 07:23:03 GMT+0000 (Coordinated Universal Time) }
]

set([elementAdapter])

By default, JavaScript sets are serialized as an empty object. Using this adapter allows you to serialize them in the same way that arrays are serialized:

const adapter = JsonAdapters.set();

console.log(adapter.adaptToJson(new Set([1, 2, 3])));

console.log(adapter.recoverFromJson([1, 2, 3]));

Output:

[1, 2, 3]

Set { 1, 2, 3 }

You can also specify an adapter to be used for mapping the elements of the set:

const adapter = JsonAdapters.set(JsonAdapters.dateTimestamp());

console.log(adapter.adaptToJson(new Set([new Date(0), new Date(1620458583563)])));

console.log(adapter.recoverFromJson([0, 1620458583563]));

Output:

[0, 1620458583563]

Set {
    Date { Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time) },
    Date { Sat May 08 2021 07:23:03 GMT+0000 (Coordinated Universal Time) }
}

Notice that calling JsonAdapters.set() without any element adapter is equivalent to JsonAdapters.set(JsonAdapters.identity()).

record(valueAdapter)

A record is a JavaScript plain object consisting of key-value pairs. In that sense, it's a similar to a Map (a.k.a. hashtable or dictionary in other programming languages), but its keys are always strings*.

* JavaScript allows to use the symbol type as a key also, but TrueJSON expects records to be in the form { string: any } (for TypeScript users: Record<string, any>).

The record adapter receives an adapter that will be applied to each of the record values, just as the array adapter does:

const adapter = JsonAdapters.record(JsonAdapters.dateTimestamp());

console.log(adapter.adaptToJson({
    start: new Date(0),
    end: new Date(1620458583563)
}));

console.log(adapter.recoverFromJson({
    start: 0,
    end: 1620458583563
}));

Output:

{
    "start": 0,
    "end": 1620458583563
}

{
    "start": Date { Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time) },
    "end": Date { Sat May 08 2021 07:23:03 GMT+0000 (Coordinated Universal Time) }
}

Configuration options

The record adapter allows to modify its default behaviour using the following configuration options:

Property Type Default value Description
strictPlainObjectCheck boolean false When true, it will throw an error if the input value of the adaptToJson() method is not a plain object.

mapAsEntries([config])

By default, JavaScript maps are serialized as an empty object. Using this adapter allows you to serialize them as an array of entries (see Map.prototype.entries()):

const map = new Map();
map.set('number', 42);
map.set('string', 'hello world');
map.set('array', [1, 2, 3]);

const adapter = JsonAdapters.mapAsEntries();

console.log(adapter.adaptToJson(map));

console.log(adapter.recoverFromJson([
    ['number', 42],
    ['string', 'hello world'],
    ['array', [1, 2, 3]]
]));

Output:

[
    ["number", 42],
    ["string", "hello world"],
    ["array", [1, 2, 3]]
]

Map {
    "number" => 42,
    "string" => "hello world",
    "array" => [1, 2, 3]
}

As map's keys and values can be any type of object, you can also specify a keyAdapter and a valueAdapter:

const map = new Map();
map.set(new Date(0), new Set([1, 2, 3]));
map.set(new Date(1620458583563), new Set([4, 5, 6]));

const adapter = JsonAdapters.mapAsEntries({
    keyAdapter: JsonAdapters.dateTimestamp(),
    valueAdapter: JsonAdapters.set(),
});

console.log(adapter.adaptToJson(map));

console.log(adapter.recoverFromJson([
    [0, [1, 2, 3]],
    [1620458583563, [4, 5, 6]]
]));

Output:

[
    [0, [1, 2, 3]],
    [1620458583563, [4, 5, 6]]
]

Map {
    Date { Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time) } => Set { 1, 2, 3 },
    Date { Sat May 08 2021 07:23:03 GMT+0000 (Coordinated Universal Time) } => Set { 4, 5, 6 }
}

Notice that calling JsonAdapters.mapAsEntries() without key and value adapters is equivalent to:

JsonAdapters.mapAsEntries({
    keyAdapter: JsonAdapters.identity(),
    valueAdapter: JsonAdapters.identity()
});

mapAsRecord([config])

By default, JavaScript maps are serialized as an empty object. Using this adapter allows you to serialize them as a plain JS object (a.k.a. record):

const map = new Map();
map.set('number', 42);
map.set('string', 'hello world');
map.set('array', [1, 2, 3]);

const adapter = JsonAdapters.mapAsRecord();

console.log(adapter.adaptToJson(map));

console.log(adapter.recoverFromJson({
    number: 42,
    string: 'hello world',
    array: [1, 2, 3]
}));

Output:

{
    "number": 42,
    "string": "hello world",
    "array": [1, 2, 3]
}

Map {
    "number" => 42,
    "string" => "hello world",
    "array" => [1, 2, 3]
}

As map's keys and values can be any type of object, you can also specify a keyAdapter and a valueAdapter:

const map = new Map();
map.set(new Date(0), new Set([1, 2, 3]));
map.set(new Date(1620458583563), new Set([4, 5, 6]));

const adapter = JsonAdapters.mapAsRecord({
    keyAdapter: JsonAdapters.isoDate(),
    valueAdapter: JsonAdapters.set(),
});

console.log(adapter.adaptToJson(map));

console.log(adapter.recoverFromJson({
    "1970-01-01T00:00:00.000Z": [1, 2, 3],
    "2021-05-08T07:23:03.563Z": [4, 5, 6]
}));

Output:

{
    "1970-01-01T00:00:00.000Z": [1, 2, 3],
    "2021-05-08T07:23:03.563Z": [4, 5, 6]
}

Map {
    Date { Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time) } => Set { 1, 2, 3 },
    Date { Sat May 08 2021 07:23:03 GMT+0000 (Coordinated Universal Time) } => Set { 4, 5, 6 }
}

Notice that calling JsonAdapters.mapAsRecord() without key and value adapters is equivalent to:

JsonAdapters.mapAsRecord({
    keyAdapter: JsonAdapters.identity(),
    valueAdapter: JsonAdapters.identity()
});

object(propertyAdapters[, config])

This adapter allows you to serialize & deserialize any plain JS object, specifying different adapters for each of its properties:

const film = {
    name: 'Harry Potter and the Deathly Hallows - Part 2',
    releaseDate: new Date('2011-07-15'),
    mainCharacters: new Set(['Harry Potter', 'Hermione Granger', 'Ron Weasley'])
};

const adapter = JsonAdapters.object({
    releaseDate: JsonAdapters.isoDate(),
    mainCharacters: JsonAdapters.set()
});

console.log(adapter.adaptToJson(film));

console.log(adapter.recoverFromJson({
    name: 'Harry Potter and the Deathly Hallows - Part 2',
    releaseDate: '2011-07-15T00:00:00.000Z',
    mainCharacters: ['Harry Potter', 'Hermione Granger', 'Ron Weasley']
}));

Output:

{
    "name": "Harry Potter and the Deathly Hallows - Part 2",
    "releaseDate": "2011-07-15T00:00:00.000Z",
    "mainCharacters": ["Harry Potter", "Hermione Granger", "Ron Weasley"]
}

{
    "name": "Harry Potter and the Deathly Hallows - Part 2",
    "releaseDate": Date { Fri Jul 15 2011 00:00:00 GMT+0000 (Coordinated Universal Time) },
    "mainCharacters": Set { "Harry Potter", "Hermione Granger", "Ron Weasley" }
}

By default, any unmapped property will be adapted using the identity adapter.

Configuration options

The object adapter allows to modify its default behaviour using the following configuration options:

Property Type Default value Description
omitUnmappedProperties boolean false When true, all unmapped properties won't be present on the resultant object.
omittedProperties string[] [] Allows to specify which properties should be omitted manually.
strictPlainObjectCheck boolean false When true, it will throw an error if the input value of the adaptToJson() method is not a plain object.

Example using omittedProperties option:

const adapter = JsonAdapters.object(
        {
            birthDate: JsonAdapters.dateTimestamp()
        },
        {
            omittedProperties: ['age']
        }
);

console.log(adapter.adaptToJson({
    name: 'John Doe',
    birthDate: new Date('1970-01-01'),
    age: 51
}));

console.log(adapter.recoverFromJson({
    name: 'John Doe',
    birthDate: 0,
    age: 51
}));

Output:

{
    "name": "John Doe",
    "birthDate": 0
}

{
    "name": "John Doe",
    "birthDate": Date { Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time) }
}

byKey(keyValuePairs)

This adapter allows you to serialize a value using its corresponding key of the provided key-value pairs object. This is specially useful when working with enumerated values:

const ScalingStrategies = {
    DEFAULT: new DefaultScalingStrategy(),
    FAST: new FastScalingStrategy(),
    SMOOTH: new SmoothScalingStrategy()
};

const adapter = JsonAdapters.byKey(ScalingStrategies);

console.log(adapter.adaptToJson(ScalingStrategies.FAST));

console.log(adapter.recoverFromJson('SMOOTH'));

Output:

"FAST"

SmoothScalingStrategy { }

If any unknown value* is passed to adaptToJson() or recoverFromJson() methods, an error is thrown. If you don't want this to happen, you can use byKeyLenient(keyValuePairs\[, fallbackKey\]) method.

byKeyLenient(keyValuePairs[, fallbackKey])

This is very similar to byKey(keyValuePairs)'s adapter. The main difference is the case where the passed value is not present in the keyValuePairs object. While the byKey(keyValuePairs) adapter will throw an error, this adapter will return undefined:

const ScalingStrategies = {
    DEFAULT: new DefaultScalingStrategy(),
    FAST: new FastScalingStrategy(),
    SMOOTH: new SmoothScalingStrategy()
};

const adapter = JsonAdapters.byKeyLenient(ScalingStrategies);

console.log(adapter.adaptToJson(new UnknownScalingStrategy()));

console.log(adapter.recoverFromJson('UNKNOWN'));

Output:

undefined

undefined

Due to this, undefined value is also a valid input for both adaptToJson() and recoverFromJson() methods:

const ScalingStrategies = {
    DEFAULT: new DefaultScalingStrategy(),
    FAST: new FastScalingStrategy(),
    SMOOTH: new SmoothScalingStrategy()
};

const adapter = JsonAdapters.byKeyLenient(ScalingStrategies);

console.log(adapter.adaptToJson(undefined));

console.log(adapter.recoverFromJson(undefined));

Output:

undefined

undefined

Alternatively, you can pass a fallback key to use when an unknown object or key is passed to the adapter:

const ScalingStrategies = {
    DEFAULT: new DefaultScalingStrategy(),
    FAST: new FastScalingStrategy(),
    SMOOTH: new SmoothScalingStrategy()
};

const adapter = JsonAdapters.byKeyLenient(ScalingStrategies, 'DEFAULT');

console.log(adapter.adaptToJson(new UnknownScalingStrategy()));

console.log(adapter.recoverFromJson('UNKNOWN'));

Output:

"DEFAULT"

DefaultScalingStrategy { }

Identity adapters

Identity adapters take their name from the concept of identity function. Those adapters doesn't really adapt its input value, but they're still able to perform some validations that will ensure the received JSON has the expected format. The following sections will cover the different identity adapters provided by TrueJSON out-of-the-box.

identity([validator])

The identity adapter takes its name from the concept of identity function. It just returns the same value it receives:

const adapter = JsonAdapters.identity();

console.log(adapter.adaptToJson(3));

console.log(adapter.recoverFromJson(3));

Output:

3

3

By default, it doesn't perform any validations, but it accepts to pass a validator function, allowing you to implement any validation logic that you may need:

const probabilityAdapter = JsonAdapters.identity(input => {

    if (typeof input !== 'number' || !Number.isFinite()) {
        throw new TypeError('input value is not a finite number');
    }

    if(input < 0 || input > 1) {
        throw new Error('input value is out of [0, 1] range');
    }

});

console.log(probabilityAdapter.adaptToJson('A text'));
console.log(probabilityAdapter.adaptToJson(3));
console.log(probabilityAdapter.adaptToJson(0.3));

console.log(probabilityAdapter.recoverFromJson(Infinity));
console.log(probabilityAdapter.recoverFromJson(-1));
console.log(probabilityAdapter.recoverFromJson(0.95));

Output:

TypeError: input value is not a finite number
Error: input value is out of [0, 1] range
0.3

TypeError: input value is not a finite number
Error: input value is out of [0, 1] range
0.95

Additionally, in the following sections you'll find some other identity adapters that perform some of the most-common type validations.

stringIdentity()

The string identity adapter simply returns the same value it receives, throwing an error if the value is not a string:

const stringIdentityAdapter = JsonAdapters.stringIdentity();

console.log(stringIdentityAdapter.adaptToJson('A text'));
console.log(stringIdentityAdapter.adaptToJson(3));

console.log(stringIdentityAdapter.recoverFromJson('Another text'));
console.log(stringIdentityAdapter.recoverFromJson(['an', 'array']));

Output:

"A text"
TypeError: input value is not a string

"Another text"
TypeError: input value is not a string

numberIdentity()

The number identity adapter simply returns the same value it receives, throwing an error if the value is not a finite number (this means that non-finite values like Infinite, -Infinite or NaN are not valid):

const integerIdentityAdapter = JsonAdapters.numberIdentity();

console.log(integerIdentityAdapter.adaptToJson(1234));
console.log(integerIdentityAdapter.adaptToJson(Infinity));

console.log(integerIdentityAdapter.recoverFromJson(-5.7));
console.log(integerIdentityAdapter.recoverFromJson({an: 'object'}));

Output:

1234
TypeError: input value is not a finite number

-5.7
TypeError: input value is not a finite number

integerIdentity()

The integer identity adapter simply returns the same value it receives, throwing an error if the value is not an integer number (this means that decimal numbers and non-finite values like 12.34, Infinite, -Infinite or NaN are not valid):

const integerIdentityAdapter = JsonAdapters.integerIdentity();

console.log(integerIdentityAdapter.adaptToJson(1234));
console.log(integerIdentityAdapter.adaptToJson(12.34));

console.log(integerIdentityAdapter.recoverFromJson(-5));
console.log(integerIdentityAdapter.recoverFromJson(NaN));

Output:

1234
TypeError: input value is not an integer

-5
TypeError: input value is not an integer

booleanIdentity()

The boolean identity adapter simply returns the same value it receives, throwing an error if the value is not a boolean:

const booleanIdentityAdapter = JsonAdapters.booleanIdentity();

console.log(booleanIdentityAdapter.adaptToJson(true));
console.log(booleanIdentityAdapter.adaptToJson('true'));

console.log(booleanIdentityAdapter.recoverFromJson(false));
console.log(booleanIdentityAdapter.recoverFromJson(0));

Output:

true
TypeError: input value is not a boolean

false
TypeError: input value is not a boolean

Handling nullish values

Extracted from MDN:

In JavaScript, a nullish value is the value which is either null or undefined.

Adapters discussed in previous sections are not designed taking nullish values into account. If you try to use them for serializing or deserializing null or undefined values, they could throw an error or return an unexpected value. Let's take a look to the behaviour of the set adapter:

const standardSetAdapter = JsonAdapters.set();

console.log(standardSetAdapter.adaptToJson(new Set([1, 2, 3])));
console.log(standardSetAdapter.adaptToJson(null));
console.log(standardSetAdapter.adaptToJson(undefined));

console.log(standardSetAdapter.recoverFromJson([1, 2, 3]));
console.log(standardSetAdapter.recoverFromJson(null));
console.log(standardSetAdapter.recoverFromJson(undefined));

Output:

[1, 2, 3]
TypeError: o is not iterable
TypeError: o is not iterable

Set { 1, 2, 3 }
TypeError: Cannot read properties of null (reading 'map')
TypeError: Cannot read properties of undefined (reading 'map')

The same applies when you write your own adapters. If you don't write your adapter having nullish values in mind, it may not work as expected when receiving one.

Fortunately, TrueJSON allows to wrap any existing adapter using a proxy adapter that handles null and undefined values:

nullishAware(adapter)

Wraps an existing adapter using a proxy that handles both null and undefined values. This proxy will return the received value when it's a nullish value; otherwise it will call the real adapter:

const nullishAwareSetAdapter = JsonAdapters.set();

console.log(nullishAwareSetAdapter.adaptToJson(new Set([1, 2, 3])));
console.log(nullishAwareSetAdapter.adaptToJson(null));
console.log(nullishAwareSetAdapter.adaptToJson(undefined));

console.log(nullishAwareSetAdapter.recoverFromJson([1, 2, 3]));
console.log(nullishAwareSetAdapter.recoverFromJson(null));
console.log(nullishAwareSetAdapter.recoverFromJson(undefined));

Output:

[1, 2, 3]
null
undefined

Set { 1, 2, 3 }
null
undefined

nullAware(adapter)

Wraps an existing adapter using a proxy that handles only null values. This proxy will return null when receiving the null value; otherwise it will call the real adapter:

const nullAwareSetAdapter = JsonAdapters.nullAware(JsonAdapters.set());

console.log(nullAwareSetAdapter.adaptToJson(new Set([1, 2, 3])));
console.log(nullAwareSetAdapter.adaptToJson(null));
console.log(nullAwareSetAdapter.adaptToJson(undefined));

console.log(nullAwareSetAdapter.recoverFromJson([1, 2, 3]));
console.log(nullAwareSetAdapter.recoverFromJson(null));
console.log(nullAwareSetAdapter.recoverFromJson(undefined));

Output:

[1, 2, 3]
null
TypeError: o is not iterable

Set { 1, 2, 3 }
null
TypeError: Cannot read properties of undefined (reading 'map')

Notice an error is thrown when using the undefined value.

undefinedAware(adapter)

Wraps an existing adapter using a proxy that handles only undefined values. This proxy will return undefined when receiving the undefined value; otherwise it will call the real adapter:

const undefinedAwareSetAdapter = JsonAdapters.undefinedAware(JsonAdapters.set());

console.log(undefinedAwareSetAdapter.adaptToJson(new Set([1, 2, 3])));
console.log(undefinedAwareSetAdapter.adaptToJson(null));
console.log(undefinedAwareSetAdapter.adaptToJson(undefined));

console.log(undefinedAwareSetAdapter.recoverFromJson([1, 2, 3]));
console.log(undefinedAwareSetAdapter.recoverFromJson(null));
console.log(undefinedAwareSetAdapter.recoverFromJson(undefined));

Output:

[1, 2, 3]
TypeError: o is not iterable
undefined

Set { 1, 2, 3 }
TypeError: Cannot read properties of null (reading 'map')
undefined

Notice an error is thrown when using the null value.

Writing your own adapter

You can write your own adapter using the JsonAdapters.custom() method:

const dateToArrayAdapter = JsonAdapters.custom({
    adaptToJson(date) {

        // Perform some validations

        if (!(date instanceof Date)) {
            throw new TypeError('input value is not a date');
        }

        // Adapt the Date object to array

        return [
            date.getFullYear(),
            date.getMonth(),
            date.getDate()
        ];

    },
    recoverFromJson(array) {

        // Perform some validations

        if (!Array.isArray(array)) {
            throw new TypeError('input value is not an array');
        }

        if (array.length !== 3) {
            throw new TypeError('input value has not the expected length');
        }

        // Recover the Date object from the deserialized array

        const [
            year,
            month,
            date
        ] = array;

        return new Date(year, month, date);

    }
});

Then, you can use it as any other adapter:

const objectAdapter = JsonAdapters.object({
    birthDate: dateToArrayAdapter
});

console.log(objectAdapter.adaptToJson({
    name: 'John Doe',
    birthDate: new Date('1970-01-01')
}));

console.log(objectAdapter.recoverFromJson({
    name: 'John Doe',
    birthDate: [1970, 0, 1]
}));

Output:

{
    "name": "John Doe",
    "birthDate": [1970, 0, 1]
}

{
    "name": "John Doe",
    "birthDate": Date { Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time) }
}

Contributing

This is a library maintained by one person, so any bug report, suggestion, pull request, or any other kind of feedback will be really appreciated :slightly_smiling_face:

Please contribute using GitHub Flow. Create a branch from the develop one, add commits, and open a pull request.

Please note we have a code of conduct, please follow it in all your interactions with the project.

If you want to get in touch with the author, you can contact me through LinkedIn or email.