JSON Anything
Did you know that JSON.stringify
and JSON.parse
can take more than one argument?
Perhaps like me you’ve done:
JSON.stringify(data, null, 2);
To output readable JSON with 2 space formatting — the correctly amount of whitespace. But what is that null
all about? Surely I’ve researched this before and forgotten.
JSON basics
Standard JSON values include the primitives:
boolean
null
number
string
And then array
and object
collections of all those and each other.
Although a Set looks very similar to an array
, and Map like an object
, they cannot be serialised as JSON. That is unless you take advantage of the mythical 2nd argument.
JSON enhanced
JSON.stringify
takes a replacer
as the 2nd argument and JSON.parse
takes a corresponding reviver
. These are two functions that receive a key/value
pair and return the value
. For replacer
the returned value
must be a standard JSON value. For the reviver
the value can be anything.
In the example below the replacer
converts a Set
and Map
into a standard JavaScript object. I’ve come up with a simple notation to retain enough information for later.
const replacer = (key, value) => {
if (value instanceof Set || value instanceof Map) {
return {
_class: value.constructor.name,
_value: [...value]
};
}
return value;
};
Let’s see it in action:
const set = new Set();
set.add('apples');
set.add('oranges');
set.add('bananas');
const map = new Map();
map.set('apples', 'red');
map.set('oranges', 'purple');
map.set('bananas', 'yellow');
const data = {set, map};
const serialized = JSON.stringify(data, replacer, 2);
console.log(serialized);
This example will output the JSON:
{
"set": {
"_class": "Set",
"_value": [
"apples",
"oranges",
"bananas"
]
},
"map": {
"_class": "Map",
"_value": [
[
"apples",
"red"
],
[
"oranges",
"purple"
],
[
"bananas",
"yellow"
]
]
}
}
The order in which entries are added to a Set and Map can be significant. My JSON conversion should retain that order. I’m using [...value]
to convert both into an array. That is shorthand for Array.from(value.values())
and Array.from(value.entries())
for a Set and Map respectively. Sets iterate over their values; Maps over their entries.
Now to get this back into JavaScript we need a reviver
that understands this notation:
const reviver = (key, value) => {
if (value && typeof value === 'object') {
if (value._class === 'Set') return new Set(value._value);
if (value._class === 'Map') return new Map(value._value);
}
return value;
};
Note that I’m checking value
is truthy because null
is technically an object type.
If you’re playing a round of code golf:
const reviver = (k, v) =>
v?._class ? new globalThis[v._class](v._value) : v;
I figured the former version was more appropriate.
Anyway, following on from my previous example:
const deserialized = JSON.parse(serialized, reviver);
console.log(deserialized);
This will output a real instance of Set
and Map
.
It’s important to note that these are not the same instances as before.
console.log(map === deserialized.map);
This equals false
.
However, they do have the exact same data.
const s1 = JSON.stringify(map, replacer);
const s2 = JSON.stringify(deserialized.map, replacer);
console.log(s1 === s2);
This equals true
.
Using this technique you could serialise almost anything as JSON providing it is “pure”. That is to say, the same input produces the same output. At least pure enough for your use case.
I’ve thrown this code into a CodePen if you want to have a play around.
See you later! I’m out to buy fruit with my JSON shopping list 😎✌️