Correctly round-tripping serialization
The accepted answer is really lovely. However,Just copy this and use it doesn't handle the edge case where an object with a. Or use dataType
property is passed to the function correctlythe npm package.
const originalValueserialize = { dataType: "Map" }; // Edge case input
const str(value) ==> JSON.stringify(originalValuevalue, replacerstringifyReplacer);
const newValuedeserialize = JSON.parse(str, revivertext);
console => JSON.logparse(originalValue, strtext, newValueparseReviver);
// bug: newValue ends up being a Map
Improved Variant
Hence, I wrote this improved version for my purposes. It uses _meta
instead of dataType
, to make conflicts rarer and if a conflict does happen, it actually handles it.
// License: CC0
function stringifyReplacer(key, value) {
if (typeof value === "object" && value !== null) {
if (value instanceof Map) {
return {
_meta: { type: "map" },
value: Array.from(value.entries()),
};
} else if (value instanceof Set) { // bonus feature!
return {
_meta: { type: "set" },
value: Array.from(value.values()),
};
} else if ("_meta" in value) {
// Escape "_meta" properties
return {
...value,
_meta: {
type: "escaped-meta",
value: value["_meta"],
},
};
}
}
return value;
}
function parseReviver(key, value) {
if (typeof value === "object" && value !== null) {
if ("_meta" in value) {
if (value._meta.type === "map") {
return new Map(value.value);
} else if (value._meta.type === "set") {
return new Set(value.value);
} else if (value._meta.type === "escaped-meta") {
// Un-escape the "_meta" property
return {
...value,
_meta: value._meta.value,
};
} else {
console.warn("Unexpected meta", value._meta);
}
}
}
return value;
}
Why is this hard?
And finallyIt should be possible to input any kind of data, a littleget valid JSON, and from there correctly reconstruct the input.
This means dealing with
- Maps that have objects as keys
new Map([ [{cat:1}, "value"] ])
. This means that any answer which uses Object.fromEntries
is probably wrong.
- Maps that have nested maps
new Map([ ["key", new Map([ ["nested key", "nested value"] ])] ])
. A lot of answers sidestep this by only answering the question and not dealing with anything beyond that.
- Mixing objects and maps
{"key": new Map([ ["nested key", "nested value"] ]) }
.
and on top of those difficulties, the serialisation format must be unambiguous. Otherwise one cannot always reconstruct the input. The top answer has one failing test case that attempts, see below.
Hence, I wrote this improved version. It uses _meta
instead of dataType
, to break as muchmake conflicts rarer and if a conflict does happen, it actually unambiguously handles it. Hopefully the code is also simple enough to easily be extended to handle other containers.
My answer does, however, not attempt to handle exceedingly cursed cases, such as possiblea map with object properties.
A test case for my answer, which demonstrates a few edge cases
const originalValue = [
new Map([['a', {
b: {
_meta: { __meta: "cat" },
c: new Map([['d', 'text']])
}
}]]),
{ _meta: { type: "map" }}
];
console.log(originalValue);
let text = JSON.stringify(originalValue, stringifyReplacer);
console.log(text);
console.log(JSON.parse(text, parseReviver));
Accepted answer not round-tripping
The accepted answer is really lovely. However, it does not round trip when an object with a dataType
property is passed it it.
// Test case for the accepted answer
const originalValue = { dataType: "Map" };
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, str, newValue);
// > Object { dataType: "Map" } , Map(0)
// Notice how the input was changed into something different