406

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?

5
  • 7
    interesting article on the topic 2ality.com/2015/08/es6-map-json.html Commented Apr 4, 2018 at 15:58
  • I was able to get this to work. The results are on Plunkr at embed.plnkr.co/oNlQQBDyJUiIQlgWUPVP. The solution uses a JSON.stringify(obj, replacerFunction) which checks to see if a Map object is being passed and converts the Map object to a Javascript object (that JSON.stringify) will then convert to a string.
    – PatS
    Commented Apr 10, 2018 at 22:05
  • 2
    If your keys are guaranteed to be strings (or numbers) and your values arrays, you can do something like [...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] */ }`, '') Commented May 10, 2018 at 8:42
  • @Oriol What if it is possible for key name to be same as default properties? obj[key] may get you something unexpected. Consider the case if (!obj[key]) obj[key] = newList; else obj[key].mergeWith(newList);. Commented Nov 20, 2018 at 15:20
  • 1
    Even MDN suggesting this Stack Overflow link Commented Nov 27, 2023 at 5:18

17 Answers 17

452

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
  • 13
    Just marked this as correct. While I don't like the fact you have to "dirty up" the data across the wire with a non-standardized dataType, I can't think of a cleaner way. Thanks.
    – rynop
    Commented Oct 9, 2020 at 18:07
  • 3
    @Pawel what is the reason for using this[key] instead of value?
    – JimiDini
    Commented Jan 21, 2021 at 14:59
  • 2
    To me there seems to be a slight problem: any ordinary object o which by chance has the property o.dataType==='Map' will also be converted to a Map when you serialize-deserialize it.
    – mkoe
    Commented Jul 15, 2021 at 19:42
  • 3
    @mkoe sure, but the probability of that is somewhere between being hit by a lightning and being hit by a lightning while hiding in a basement
    – Pawel
    Commented Sep 3, 2021 at 13:40
  • 2
    If anyone is tempted to use Object.fromEntries() as stated in the MDN Docs I highly advise against it. You can parse a Map to an Object but not back! It will throw a object is not iterable error.
    – Megajin
    Commented Mar 21, 2022 at 13:49
173

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
  • 17
    This does not convert to a JSON object, but instead to an Array of arrays. Not the same thing. See Evan Carroll's answer below for a more complete answer.
    – Sat Thiru
    Commented May 8, 2019 at 14:52
  • 13
    @SatThiru An array of tuples is the customary representation of 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.
    – Bergi
    Commented May 8, 2019 at 16:42
  • Bergi, please note that OP said "My keys are guaranteed to be strings".
    – Sat Thiru
    Commented May 10, 2019 at 17:20
  • 14
    @SatThiru In that case, use JSON.stringify(Object.fromEntries(map.entries())) and new Map(Object.entries(JSON.parse(jsonText)))
    – Bergi
    Commented May 10, 2019 at 17:43
  • 6
    @Drenai Then don't use 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.
    – Bergi
    Commented Mar 13, 2020 at 18:22
73

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.
14
  • 56
    for the curious-in the latest chrome, any map serializes into '{}'
    – Capaj
    Commented Jan 6, 2016 at 16:20
  • 14
    "It might be faster" - Do you have any source on that? I'm imagining a simple hash-map must be faster than a full blown object, but I have no proof. :)
    – Lilleman
    Commented Feb 11, 2016 at 18:01
  • 3
    @Xplouder That test uses expensive hasOwnProperty. Without that, Firefox iterates objects much faster than maps. Maps are still faster on Chrome, though. jsperf.com/es6-map-vs-object-properties/95
    – Oriol
    Commented Mar 18, 2016 at 13:49
  • 6
    Just passing by and figure out my problem thanks to this. I really wish to move to a farm and leave all this behind, sometimes.
    – napolux
    Commented Aug 10, 2020 at 14:38
  • 3
    While this answer definitely points out the tricky bits, it most certainly is not "impossible" as the accepted answer demonstrates.
    – Stefnotch
    Commented Jul 28, 2022 at 15:52
52

While 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
  • 1
    > Going to an JavaScript Object < Shouldn't it have code to handle keys such as __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.
    – roim
    Commented Oct 29, 2021 at 23:40
  • As pointed out in Oriol's answer, this is incorrect. Map keys can be objects, which this answer doesn't handle.
    – Stefnotch
    Commented Jul 28, 2022 at 15:55
35

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));
2
  • 16
    This'll work for a one-dimensional Map, but not for an n-dimensional map.
    – mattsven
    Commented May 12, 2020 at 19:01
  • My answer below works for an n-dimensional map.
    – Cody
    Commented May 29 at 17:12
30

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
  • This one is nice but does require you to target ES2019. Commented Jun 23, 2021 at 11:36
  • 1
    Careful, this is just good if you want to go one way.Object.fromEntries() as stated in the MDN Docs you can parse a Map to an Object but not back! It will throw a object is not iterable error.
    – Megajin
    Commented Mar 21, 2022 at 13:53
  • @Megajin Object.fromEntries() is non-destructive, so you will still have your original Map intact. Commented Apr 6, 2022 at 7:35
  • 1
    @AlokSomani yes, you are right. However if you want to parse a JSON (or the newly created Object) back, it won't work.
    – Megajin
    Commented Apr 6, 2022 at 12:12
  • 1
    Just for additional information, this is totally ok for Typescript users which are able to make sure that the whole map structure (meaning nested objects as well) have string keys Map<string, otherType> Commented Mar 24, 2023 at 14:02
17

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;
}

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 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, 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

  1. JSON.stringify(data, acceptedAnswerReplacer) and send it over the network.
  2. 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
16

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
  • 1
    I'm not sure that many will be satisfied with extending the core map class just to serialize it to a json...
    – vasia
    Commented Apr 5, 2020 at 21:36
  • 4
    They don't have to be, but it's a more SOLID way of doing it. Specifically, this aligns with the LSP and OCP principles of SOLID. That is, the native Map is being extended, not modified, and one can still use Liskov Substitution (LSP) with a native Map. Granted, it's more OOP than a lot of novices or staunch Functional Programming people would prefer, but at least it's beset upon a tried & true baseline of fundamental software design principles. If you wanted to implement Interface Segregation Principle (ISP) of SOLID, you can have a small IJSONAble interface (using TypeScript, of course).
    – Cody
    Commented Apr 5, 2020 at 22:47
  • 1
    Yes, this is a good solution since it can produce specific serialisation variations. You can do 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.
    – VLAZ
    Commented Feb 19 at 9:03
12

Stringify a Map instance (objects as keys are OK):

JSON.stringify([...map])

or

JSON.stringify(Array.from(map))

or

JSON.stringify(Array.from(map.entries()))

output format:

// [["key1","value1"],["key2","value2"]]
5

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
  • 7
    Warning: Map can have non-string values as keys. This will not work if your Map keys are non-stringify-able types themselves : JSON.stringify(Object.fromEntries(new Map([['s', 'r'],[{s:3},'g']]))) becomes '{"s":"r","[object Object]":"g"}'
    – asp47
    Commented Jan 5, 2022 at 22:09
4

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
  • 1
    Without testing your answer, I already appreciate how it brings attention to the problem of nested Maps. Even if you successfully convert this to JSON, any parsing done in the future has to have explicit awareness that the JSON was originally a 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. Commented Nov 30, 2019 at 6:53
  • More about that here. Commented Nov 30, 2019 at 9:06
4

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
  • This replacer function is the only thing that worked for me. Thanks! Commented Jun 25 at 11:15
4

You cannot call JSON.stringify on Map or Set.

You will need to convert:

  • the Map into a primitive Object, using Object.fromEntries, or
  • the Set into a primitive Array, 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
  • 2
    There's a much easier way of serializing Maps and Sets to JSON, since JSON.stringify and JSON.parse have a second argument that lets you add custom rules. See my answer for an answer that correctly round-trips in all cases.
    – Stefnotch
    Commented Jan 23, 2023 at 15:09
3

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
  • This is the right answer unless I am missing something. All these other methods are making an absolute meal out of this.
    – MSOACC
    Commented Jan 10, 2023 at 14:03
  • 1
    This answer adds nothing new compared to Alok Somani's answer. Plus, it does not handle nested maps. It also has the same bug as Rakesh Singh Balhara's answer.
    – Stefnotch
    Commented Jan 23, 2023 at 15:05
0
JSON.stringify(errorCodes, (_, value) =>
  value instanceof Map || value instanceof Set ? Array.from(value) : value
);
3
  • 1
    It will work correctly with nested Maps because if a Map is converted into an Array, the Array members (including nested Maps) will be handled by the same logic. Commented Apr 23 at 7:25
  • That is a good point, I'm sorry for missing that. Let me remove my comment and add a new, factually correct one.
    – Stefnotch
    Commented Apr 23 at 13:37
  • This answer is very simple, but could be improved with an explanation. It also is only applicable when round-tripping is not a requirement, as there is no unambiguous way of JSON.parse()ing the result.
    – Stefnotch
    Commented Apr 23 at 13:39
-1

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
  • 1
    This turns objects into maps, which isn't what one wants. Try it with 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.
    – Stefnotch
    Commented Jan 23, 2023 at 15:03
-6

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.

4
  • 1
    Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
    – Community Bot
    Commented Oct 20, 2021 at 13:43
  • 2
    This sounds like a nice 10,000 foot process overview, but an actual implementation would be much more useful.
    – TylerH
    Commented Oct 20, 2021 at 15:51
  • Well, it was more food for thought really than a total solution. Sorry I'm new here and not sure how to add my code to the comments as yet.
    – Cbaskey.it
    Commented Oct 21, 2021 at 3:35
  • I don't get, why this is related to the question?
    – Steven Shi
    Commented Oct 29, 2021 at 0:10

Not the answer you're looking for? Browse other questions tagged or ask your own question.