I'd like to start using ES6 Map instead of JS objects but I'm being held back because I can't figure out how to JSON.stringify() a Map. My keys are guaranteed to be strings and my values will always be listed. Do I really have to write a wrapper method to serialize?
19 Answers
Both JSON.stringify and JSON.parse support a second argument. replacer and reviver respectively. With replacer and reviver below it's possible to add support for native Map object, including deeply nested values
function replacer(key, value) {
if(value instanceof Map) {
return {
dataType: 'Map',
value: Array.from(value.entries()), // or with spread: value: [...value]
};
} else {
return value;
}
}
function reviver(key, value) {
if(typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
return new Map(value.value);
}
}
return value;
}
Usage:
const originalValue = new Map([['a', 1]]);
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, newValue);
Deep nesting with combination of Arrays, Objects and Maps
const originalValue = [
new Map([['a', {
b: {
c: new Map([['d', 'text']])
}
}]])
];
const str = JSON.stringify(originalValue, replacer);
const newValue = JSON.parse(str, reviver);
console.log(originalValue, newValue);
22 Comments
dataType, I can't think of a cleaner way. Thanks.this[key] instead of value?You can't directly stringify the Map instance as it doesn't have any properties, but you can convert it to an array of tuples:
jsonText = JSON.stringify(Array.from(map.entries()));
For the reverse, use
map = new Map(JSON.parse(jsonText));
5 Comments
Maps, it goes well with the constructor and iterator. Also it is the only sensible representation of maps that have non-string keys, and object would not work there.JSON.stringify(Object.fromEntries(map.entries())) and new Map(Object.entries(JSON.parse(jsonText)))Obect.fromEntries, and use the code from my main answer instead of the one from the comment. The code that builds an object literal was in response to Sat Thiru, who gave the case that the keys are strings.You can't.
The keys of a map can be anything, including objects. But JSON syntax only allows strings as keys. So it's impossible in a general case.
My keys are guaranteed to be strings and my values will always be lists
In this case, you can use a plain object. It will have these advantages:
- It will be able to be stringified to JSON.
- It will work on older browsers.
- It might be faster.
15 Comments
hasOwnProperty. Without that, Firefox iterates objects much faster than maps. Maps are still faster on Chrome, though. jsperf.com/es6-map-vs-object-properties/95While there is no method provided by ecmascript yet, this can still be done using JSON.stringify if you map the Map to a JavaScript primitive. Here is the sample Map we'll use.
const map = new Map();
map.set('foo', 'bar');
map.set('baz', 'quz');
Going to an JavaScript Object
You can convert to JavaScript Object literal with the following helper function.
const mapToObj = m => {
return Array.from(m).reduce((obj, [key, value]) => {
obj[key] = value;
return obj;
}, {});
};
JSON.stringify(mapToObj(map)); // '{"foo":"bar","baz":"quz"}'
Going to a JavaScript Array of Objects
The helper function for this one would be even more compact
const mapToAoO = m => {
return Array.from(m).map( ([k,v]) => {return {[k]:v}} );
};
JSON.stringify(mapToAoO(map)); // '[{"foo":"bar"},{"baz":"quz"}]'
Going to Array of Arrays
This is even easier, you can just use
JSON.stringify( Array.from(map) ); // '[["foo","bar"],["baz","quz"]]'
2 Comments
__proto__? Or you can damage the entire environment by trying to serialize such a map. Alok's response doesn't suffer from this, I believe.Using spread sytax Map can be serialized in one line:
JSON.stringify([...new Map()]);
and deserialize it with:
let map = new Map(JSON.parse(map));
1 Comment
Given your example is a simple use case in which keys are going to be simple types, I think this is the easiest way to JSON stringify a Map.
JSON.stringify(Object.fromEntries(map));
The way I think about the underlying data structure of a Map is as an array of key-value pairs (as arrays themselves). So, something like this:
const myMap = new Map([
["key1", "value1"],
["key2", "value2"],
["key3", "value3"]
]);
Because that underlying data structure is what we find in Object.entries, we can utilize the native JavaScript method of Object.fromEntries() on a Map as we would on an Array:
Object.fromEntries(myMap);
/*
{
key1: "value1",
key2: "value2",
key3: "value3"
}
*/
And then all you're left with is using JSON.stringify() on the result of that.
5 Comments
Object.fromEntries() is non-destructive, so you will still have your original Map intact.Map<string, otherType>Correctly round-tripping serialization
Just copy this and use it. Or use the npm package.
const serialize = (value) => JSON.stringify(value, stringifyReplacer);
const deserialize = (text) => JSON.parse(text, parseReviver);
// 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;
}
Performance?
There is a version that is equally high quality, but has better performance (tested in Chrome and Firefox). If that matters to you, then please check it out!
https://stackoverflow.com/a/79016027/3492994
Why is this hard?
It should be possible to input any kind of data, get 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 usesObject.fromEntriesis 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, see below.
Hence, I wrote this improved version. It uses _meta instead of dataType, to make 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 a 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. That can make it dangerous to use in certain circumstances, such as
JSON.stringify(data, acceptedAnswerReplacer)and send it over the network.- Naive network handler automatically JSON decodes it. From this point on forth, you cannot safely use the accepted answer with the decoded data, since doing so would cause lots of sneaky issues.
This answer uses a slightly more complex scheme to fix such issues.
// 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
Comments
A Better Solution
// somewhere...
class Klass extends Map {
toJSON() {
var object = { };
for (let [key, value] of this) object[key] = value;
return object;
}
}
// somewhere else...
import { Klass as Map } from '@core/utilities/ds/map'; // <--wherever "somewhere" is
var map = new Map();
map.set('a', 1);
map.set('b', { datum: true });
map.set('c', [ 1,2,3 ]);
map.set( 'd', new Map([ ['e', true] ]) );
var json = JSON.stringify(map, null, '\t');
console.log('>', json);
Output
> {
"a": 1,
"b": {
"datum": true
},
"c": [
1,
2,
3
],
"d": {
"e": true
}
}
Hope that is less cringey than the answers above.
3 Comments
IJSONAble interface (using TypeScript, of course).new MyMap1(someMap) or new MyMap2(someMap) to clone and extend exiting maps easily. And then MyMap1 and MyMap2 can have different serialisation. For example, one might just serialise to a list of tuples, the other to an object. Furthermore, there can be special handling for serialising keys (since those could be arbitrary objects) or similar. The replacer option in JSON.stringify works OK but if it has to handle multiple types, it becomes cumbersome.Just want to share my version for both Map and Set JSON.stringify only. I'm sorting them, useful for debugging...
function replacer(key, value) {
if (value instanceof Map) {
const reducer = (obj, mapKey) => {
obj[mapKey] = value.get(mapKey);
return obj;
};
return [...value.keys()].sort().reduce(reducer, {});
} else if (value instanceof Set) {
return [...value].sort();
}
return value;
}
Usage:
const map = new Map();
const numbers= new Set()
numbers.add(3);
numbers.add(2);
numbers.add(3);
numbers.add(1);
const chars= new Set()
chars.add('b')
chars.add('a')
chars.add('a')
map.set("numbers",numbers)
map.set("chars",chars)
console.log(JSON.stringify(map, replacer, 2));
Result:
{
"chars": [
"a",
"b"
],
"numbers": [
1,
2,
3
]
}
1 Comment
The very simple way.
const map = new Map();
map.set('Key1', "Value1");
map.set('Key2', "Value2");
console.log(Object.fromEntries(map));
` Output:-
{"Key1": "Value1","Key2": "Value2"}
1 Comment
JSON.stringify(Object.fromEntries(new Map([['s', 'r'],[{s:3},'g']]))) becomes '{"s":"r","[object Object]":"g"}'Below solution works even if you have nested Maps
function stringifyMap(myMap) {
function selfIterator(map) {
return Array.from(map).reduce((acc, [key, value]) => {
if (value instanceof Map) {
acc[key] = selfIterator(value);
} else {
acc[key] = value;
}
return acc;
}, {})
}
const res = selfIterator(myMap)
return JSON.stringify(res);
}
2 Comments
Map and (even worse) that each sub-map (it contains) was also originally a map. Otherwise, there's no way to be sure that an array of pairs isn't just intended to be exactly that, instead of a Map. Hierarchies of objects and arrays do not carry this burden when parsed. Any proper serialization of Map would explicitly indicate that it is a Map.You cannot call JSON.stringify on Map or Set.
You will need to convert:
- the
Mapinto a primitiveObject, usingObject.fromEntries, or - the
Setinto a primitiveArray, using the spread operator[...]
…before calling JSON.stringify
Map
const
obj = { 'Key1': 'Value1', 'Key2': 'Value2' },
map = new Map(Object.entries(obj));
map.set('Key3', 'Value3'); // Add a new entry
// Does NOT show the key-value pairs
console.log('Map:', JSON.stringify(map));
// Shows the key-value pairs
console.log(JSON.stringify(Object.fromEntries(map), null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
Set
const
arr = ['Value1', 'Value2'],
set = new Set(arr);
set.add('Value3'); // Add a new item
// Does NOT show the values
console.log('Set:', JSON.stringify(set));
// Show the values
console.log(JSON.stringify([...set], null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
toJSON method
If you want to call JSON.stringify on a class object, you will need to override the toJSON method to return your instance data.
class Cat {
constructor(options = {}) {
this.name = options.name ?? '';
this.age = options.age ?? 0;
}
toString() {
return `[Cat name="${this.name}", age="${this.age}"]`
}
toJSON() {
return { name: this.name, age: this.age };
}
static fromObject(obj) {
const { name, age } = obj ?? {};
return new Cat({ name, age });
}
}
/*
* JSON Set adds the missing methods:
* - toJSON
* - toString
*/
class JSONSet extends Set {
constructor(values) {
super(values)
}
toString() {
return super
.toString()
.replace(']', ` ${[...this].map(v => v.toString())
.join(', ')}]`);
}
toJSON() {
return [...this];
}
}
const cats = new JSONSet([
Cat.fromObject({ name: 'Furball', age: 2 }),
Cat.fromObject({ name: 'Artemis', age: 5 })
]);
console.log(cats.toString());
console.log(JSON.stringify(cats, null, 2));
.as-console-wrapper { top: 0; max-height: 100% !important; }
1 Comment
Even Better than Correctly round-tripping serialization
The top-voted answer is not really the correct one, as has been already described in detail by Stefnotch in their answer here.
I'm providing my own drop-in solution that can be easily copy-pasted into any project (and can be used freely with a reference to this answer somewhere in the comments):
/**
* Provides a `JSON.stringify` replacer function that supports Map and Set object serialization.
*
* This replacer function fully supports Map and Set nesting as well as mixed values (including
* regular objects) as opposed to the popular solution recommended at MDN and described in more
* detail here: https://stackoverflow.com/q/29085197.
*
* Map and Set objects are serialized as plain JSON arrays where the first element is a special
* token string (`@Map` or `@Set`, respectively), and all subsequent elements are either `[key,
* value]` arrays (with the Map contents) or just `value` sequences (with the Set contents). To
* avoid ambiguity, regular arrays with the first element equal to one of the token strings will
* have the `@Array` token string prepended to their contents when serialized.
*
* The reverse #jsonMapSetReviver function will undo these operations when parsing the resulting
* JSON data, so it must always be used when deserializing data serialized with #jsonMapSetReplacer
* (even if there are no Map or Set objects, because there may be arrays with `@Array` prepended).
*
* This implementation seems to be faster than other alternatives almost in all cases (taking its
* functionality into account). It is also published at https://stackoverflow.com/a/79016027/3579458
* (there are some tests).
*/
function jsonMapSetReplacer (_, value_)
{
if (typeof (value_) === 'object')
{
if (value_ instanceof Map)
{
value_ = Array.from (value_);
value_.unshift ('@Map');
}
else if (value_ instanceof Set)
{
value_ = Array.from (value_);
value_.unshift ('@Set');
}
else if (Array.isArray (value_) && value_.length > 0 &&
(value_ [0] === '@Map' || value_ [0] === '@Set' || value_ [0] === '@Array'))
{
value_ = value_.slice ();
value_.unshift ('@Array');
}
}
return value_;
}
/**
* Provides a `JSON.parse` reviver function that supports Map and Set object deserialization.
*
* Must be used to deserialize JSON data serialized using #jsonMapSetReplacer.
*/
function jsonMapSetReviver (_, value_)
{
if (Array.isArray (value_) && value_.length > 0)
{
let isMap, isSet;
if ((isMap = value_ [0] === '@Map') || (isSet = value_ [0] === '@Set') || value_ [0] === '@Array')
{
value_.shift ();
if (isMap)
value_ = new Map (value_);
else if (isSet)
value_ = new Set (value_);
}
}
return value_;
}
This solution has the following benefits over other variants presented here:
It supports nested
MapandSetobjects and should properly support all kinds of mixing (maps with objects, objects with maps etc).It doesn't use relatively "heavy" object machinery to encapsulate
MapandSet(and yet it protects from possible conflicts in values matching encapsulation tags) — just plain arrays. This should make it faster both to serialize and deserialize (some tests, "Array Based 2" is the latest version, "Map Based" is Stefnotch's solution).It doesn't use the spread operator that is known to be much slower than other primitives when it's needed to prepend items to the beginning of the array (look for tests in the comments to other answers) — it only uses
slice,shiftandunshiftmodifying the original array in place where possible instead of creating a copy (e.g. when deserializing). This should also give some speed boost (and use memory more efficiently), especially on large data sets.The output overhead due to encapsulation and escaping should be very small — it is used only when absolutely necessary (and it will be just a single additional short array element in all cases). For non-
Mapand non-Setobjects and for almost all "normal" arrays there will be no overhead at all, compared to the standard JSON serialization.
UPDATE
I removed support for serializing undefined values. I found out that JSON.parse doesn't play well with undefined in arrays: when it gets undefined from the reviver, it simply deletes the respective element (effectively creating a hole) instead of just storing undefined there. Not what one wants.
8 Comments
@Array that will then get removed from it when deserializing, giving back the same ["@Set", "@Set"]. Regarding shift and unshift. Slowest compared to what? If we compare them to other array mutating primitives, they are the fastest to my knowledge (and memory efficient as they don't create copies). However, your comment gave me an idea of how to make it even better by completely avoiding array mutation, both in stringify and parse. I will check that, thanks!shift and unshift, they are slow because they need to move all subsequent array elements. They're slow compared to push and pop.push and pop are faster than anything else since no memory is moved, just the end-of-array pointer is set to a new value - when there is still some preallocated space, at least). I've also removed undefined support as it turns out to be useless (see the UPDATE section) and made code even shorter a bit. It became a tiny bit faster too, check jsperf.app/cojiwe/5 (although it seems to be within the margin of error).The following method will convert a Map to a JSON string:
public static getJSONObj(): string {
return JSON.stringify(Object.fromEntries(map));
}
Example:
const x = new Map();
x.set("SomeBool", true);
x.set("number1", 1);
x.set("anObj", { name: "joe", age: 22, isAlive: true });
const json = getJSONObj(x);
// Output:
// '{"SomeBool":true,"number1":1,"anObj":{"name":"joe","age":222,"isAlive":true}}'
2 Comments
JSON.stringify(errorCodes, (_, value) =>
value instanceof Map || value instanceof Set ? Array.from(value) : value
);
2 Comments
JSON.parse()ing the result.I really don't know why there are so many long awesers here. This short version solved my problem:
const data = new Map()
data.set('visible', true)
data.set('child', new Map())
data.get('child').set('visible', false)
const str = JSON.stringify(data, (_, v) => v instanceof Map ? Object.fromEntries(v) : v)
// '{"visible":true,"child":{"visible":false}}'
const recovered = JSON.parse(str, (_, v) => typeof v === 'object' ? new Map(Object.entries(v)) : v)
// Map(2) { 'visible' => true, 'child' => Map(1) { 'visible' => false } }
1 Comment
const data = {visible: true, child: {visible: false}}. You'll recover two nested maps instead of recovering objects. Check out my answer for an actually correct implementation.Recursive JSON stringify for MAP
You can use the second argument "replacer" of JSON.stringify :
JSON.stringify(value, replacer)
replacer (Optional) :
- A function that alters the behavior of the stringification process, or an array of strings and numbers that specifies properties of value to be included in the output.
[Read more on mdn...]
If you detect a map with instanceof Map, you will apply Object.fromEntries(value) like this :
function JSON_MapStringify(obj) {
return JSON.stringify(obj, function (key, value) {
if (value instanceof Map) {
return Object.fromEntries(value)
}
return value
})
}
Demo
For:
{
"foo": Map (1) {
"o": Map(1) {
"k": 2
}
},
"bar": 1
}
See the result:
default (JSON.stringify) : {"foo":{},"bar":1}
custom (JSON_MapStringify): {"foo":{"o":{"k":2}},"bar":1}
const sub = new Map()
sub.set('k', 2)
const main = new Map()
main.set('o', sub)
const obj = { foo: main, bar: 1}
console.log('default (JSON.stringify) :', JSON.stringify(obj))
function JSON_MapStringify(obj) {
return JSON.stringify(obj, function (key, value) {
if (value instanceof Map) {
return Object.fromEntries(value)
}
return value
})
}
console.log('custom (JSON_MapStringify):', JSON_MapStringify(obj))
Although there would be some scenarios where if you were the creator of the map you would write your code in a separate 'src' file and save a copy as a .txt file and, if written concisely enough, could easily be read in, deciphered, and added to server-side.
The new file would then be saved as a .js and a reference to it sent back from the server. The file would then reconstruct itself perfectly once read back in as JS. The beauty being that no hacky iterating or parsing is required for reconstruction.
[...someMap.entries()].join(';'); for something more complex you could try something similar using something like[...someMap.entries()].reduce((acc, cur) => acc + `${cur[0]}:${/* do something to stringify cur[1] */ }`, '')obj[key]may get you something unexpected. Consider the caseif (!obj[key]) obj[key] = newList; else obj[key].mergeWith(newList);.