93
var a = new Map([[ 'a', 1 ]]);
a.get('a') // 1

var forStorageSomewhere = JSON.stringify(a);
// Store, in my case, in localStorage.

// Later:
var a = JSON.parse(forStorageSomewhere);
a.get('a') // TypeError: undefined is not a function

Unfortunatly JSON.stringify(a); simply returns '{}', which means a becomes an empty object when restored.

I found es6-mapify that allows up/down-casting between a Map and a plain object, so that might be one solution, but I was hoping I would need to resort to an external dependency simply to persist my map.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Letharion
  • 4,067
  • 7
  • 31
  • 42
  • here's how to do it without an assumption of knowing what you're serialising https://stackoverflow.com/a/57730562/696535 – Pawel Sep 04 '19 at 22:54

10 Answers10

127

Assuming that both your keys and your values are serialisable,

localStorage.myMap = JSON.stringify(Array.from(map.entries()));

should work. For the reverse, use

map = new Map(JSON.parse(localStorage.myMap));
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    Since I do initialize the Map with an array, you'd think I would have though of this myself. :) Thanks. – Letharion Mar 08 '15 at 10:30
  • 3
    Just tried this in Chrome and `.entries()` wasn't necessary; this seemed to do the same thing: `localStorage.myMap = JSON.stringify(Array.from(map));` – bmaupin Mar 15 '18 at 18:03
  • 2
    @bmaupin Yes, the default `[Symbol.iterator]` that `Array.from` uses is the same as `.entries` on a Map, but I wanted to spell it out so that it's obvious we are using an iterator here. – Bergi Mar 15 '18 at 19:58
45

Clean as a whistle:

JSON.stringify([...myMap])
Oded Breiner
  • 28,523
  • 10
  • 105
  • 71
  • 8
    This is just splendid. I wish I could upvote more than once. For those reading, you can then deserialise it by simply doing: `let deserialized = new Map(JSON.parse(serialisedMap));` – Emanuele Feliziani Feb 26 '18 at 13:56
  • 1
    If doing this in typescript, you'll need to add `"downlevelIteration": true,` to your `compilerOptions` in tsconfig.json, otherwise it will throw errors. – Mordred Oct 19 '21 at 05:13
  • Thx a lot! This was the only solution while trying to find a way to conver Map to JSON and back to Map while mainting insertion order – André Clérigo Sep 10 '22 at 19:19
14

Usually, serialization is only useful if this property holds

deserialize(serialize(data)).get(key) ≈ data.get(key)

where a ≈ b could be defined as serialize(a) === serialize(b).

This is satisfied when serializing an object to JSON:

var obj1 = {foo: [1,2]},
    obj2 = JSON.parse(JSON.stringify(obj1));
obj1.foo; // [1,2]
obj2.foo; // [1,2] :)
JSON.stringify(obj1.foo) === JSON.stringify(obj2.foo); // true :)

And this works because properties can only be strings, which can be losslessly serialized into strings.

However, ES6 maps allow arbitrary values as keys. This is problematic because, objects are uniquely identified by their reference, not their data. And when serializing objects, you lose the references.

var key = {},
    map1 = new Map([ [1,2], [key,3] ]),
    map2 = new Map(JSON.parse(JSON.stringify([...map1.entries()])));
map1.get(1); // 2
map2.get(1); // 2 :)
map1.get(key); // 3
map2.get(key); // undefined :(

So I would say in general it's not possible to do it in an useful way.

And for those cases where it would work, most probably you can use a plain object instead of a map. This will also have these advantages:

  • It will be able to be stringified to JSON without losing key information.
  • It will work on older browsers.
  • It might be faster.
Oriol
  • 274,082
  • 63
  • 437
  • 513
  • Very interesting. +1 I'm not currently working on this project any more, but I'll keep this in mind for the future. While it's been a while, so I don't exactly remember why I picked a map, I believe it had something to do with it fitting precisely to my use case, which I imagined would make it a fast option as well. Could you elaborate on the "might be faster" bit? :) – Letharion Jan 29 '16 at 13:23
  • @Letharion I remember reading that a certain version version of Firefox detected when all keys in a map are strings, and then it could be optimized more (like objects). Then I guess maps will be slower on browsers without this kind of optimization. But that's implementation-dependent, and I haven't done any test to measure performance. – Oriol Jan 29 '16 at 13:29
9

Building off of Oriol's answer, we can do a little better. We can still use object references for keys as long as the there is primitive root or entrance into the map, and each object key can be transitively found from that root key.

Modifying Oriol's example to use Douglas Crockford's JSON.decycle and JSON.retrocycle we can create a map that handles this case:

var key = {},
    map1 = new Map([ [1, key], [key, 3] ]),
    map2 = new Map(JSON.parse(JSON.stringify([...map1.entries()]))),
    map3 = new Map(JSON.retrocycle(JSON.parse(JSON.stringify(JSON.decycle([...map1.entries()])))));
map1.get(1); // key
map2.get(1); // key
map3.get(1); // key
map1.get(map1.get(1)); // 3 :)
map2.get(map2.get(1)); // undefined :(
map3.get(map3.get(1)); // 3 :)

Decycle and retrocycle make it possible to encode cyclical structures and dags in JSON. This is useful if we want to build relations between objects without creating additional properties on those objects themselves, or want to interchangeably relate primitives to objects and visa-versa, by using an ES6 Map.

The one pitfall is that we cannot use the original key object for the new map (map3.get(key); would return undefined). However, holding the original key reference, but a newly parsed JSON map seems like a very unlikely case to ever have.

Community
  • 1
  • 1
jgawrych
  • 3,322
  • 1
  • 28
  • 38
  • I would suggest that if more Stack Overflow answers had the form, and quality of this one - and moreover be as understatedly complete in their degree of utility - we should have quite the excellent JavaScript resource. – corse32 Oct 02 '17 at 06:14
5

If you implement your own toJSON() function for any class objects you have then just regular old JSON.stringify() will just work!

Maps with Arrays for keys? Maps with other Map as values? A Map inside a regular Object? Maybe even your own custom class; easy.

Map.prototype.toJSON = function() {
    return Array.from(this.entries());
};

That's it! prototype manipulation is required here. You could go around adding toJSON() manually to all your non-standard stuff, but really you're just avoiding the power of JS

DEMO

test = {
    regular : 'object',
    map     : new Map([
        [['array', 'key'], 7],
        ['stringKey'     , new Map([
            ['innerMap'    , 'supported'],
            ['anotherValue', 8]
        ])]
    ])
};
console.log(JSON.stringify(test));

outputs:

{"regular":"object","map":[[["array","key"],7],["stringKey",[["innerMap","supported"],["anotherValue",8]]]]}

Deserialising all the way back to real Maps isn't as automatic, though. Using the above resultant string, I'll remake the maps to pull out a value:

test2 = JSON.parse(JSON.stringify(test));
console.log((new Map((new Map(test2.map)).get('stringKey'))).get('innerMap'));

outputs

"supported"

That's a bit messy, but with a little magic sauce you can make deserialisation automagic too.

Map.prototype.toJSON = function() {
    return ['window.Map', Array.from(this.entries())];
};
Map.fromJSON = function(key, value) {
    return (value instanceof Array && value[0] == 'window.Map') ?
        new Map(value[1]) :
        value
    ;
};

Now the JSON is

{"regular":"object","test":["window.Map",[[["array","key"],7],["stringKey",["window.Map",[["innerMap","supported"],["anotherValue",8]]]]]]}

And deserialising and use is dead simple with our Map.fromJSON

test2 = JSON.parse(JSON.stringify(test), Map.fromJSON);
console.log(test2.map.get('stringKey').get('innerMap'));

outputs (and no new Map()s used)

"supported"

DEMO

Hashbrown
  • 12,091
  • 8
  • 72
  • 95
  • Map.fromJSON doesn't seem to work when there are Maps within Maps. I made a utility function `deserializeMapString()` to handle a multidimensional Map [here](https://github.com/navadmin-viewer/navadmin-viewer.github.io/blob/master/js/serialization.js#L12) – ansonl Dec 05 '22 at 02:29
  • 1
    @ansonl it does work for nested maps, that's the whole point of the second part of the answer (after the horizontal rule). Click `DEMO` at the end, it prints `supported` which was a value of a Map within a Map, after running the `test` variable through json parse without relying on nonstandard functions like "deserializeMapString()". Did you perhaps forget to pass `Map.fromJSON` into `JSON.parse()` when trying it? – Hashbrown Dec 05 '22 at 02:59
3

The accepted answer will fail when you have multi dimentional Maps. One should always keep in mind that, a Map object can take another Map object as a key or value.

So a better and safer way of handling this job could be as follows;

function arrayifyMap(m){
  return m.constructor === Map ? [...m].map(([v,k]) => [arrayifyMap(v),arrayifyMap(k)])
                               : m;
}

Once you have this tool then you can always do like;

localStorage.myMap = JSON.stringify(arrayifyMap(myMap))
Redu
  • 25,060
  • 6
  • 56
  • 76
3
// store
const mapObj = new Map([['a', 1]]);
localStorage.a = JSON.stringify(mapObj, replacer);

// retrieve
const newMapObj = JSON.parse(localStorage.a, reviver);

// required replacer and reviver functions
function replacer(key, value) {
  const originalObject = this[key];
  if(originalObject instanceof Map) {
    return {
      dataType: 'Map',
      value: Array.from(originalObject.entries()), // or with spread: value: [...originalObject]
    };
  } else {
    return value;
  }
}
function reviver(key, value) {
  if(typeof value === 'object' && value !== null) {
    if (value.dataType === 'Map') {
      return new Map(value.value);
    }
  }
  return value;
}

I wrote here the explanation about replacer and reviver functions here https://stackoverflow.com/a/56150320/696535

This code will work for any other value like regular JSON.stringify so there's no assumption that the serialised object must be a Map. It can also be a Map deeply nested in an array or an object.

Pawel
  • 16,093
  • 5
  • 70
  • 73
1

One thing that is being left outis that Map is an ORDERED structure - i.e. when iterating the first item entered would be the first listed.

This is NOT like a Javascript Object. I required this type of structure (so i used Map) and then to find out that JSON.stringify doesn't work is painful (but understandable).

I ended up making a 'value_to_json' function, which means parsing EVERYTHING - using JSON.stringify only for the most basic 'types'.

Unfortunately subclassing MAP with a .toJSON() doesn't work as it excepts a value not a JSON_string. Also it is considered legacy.

My use case would be exceptional though.

related:

function value_to_json(value) {
  if (value === null) {
    return 'null';
  }
  if (value === undefined) {
    return 'null';
  }
  //DEAL WITH +/- INF at your leisure - null instead..

  const type = typeof value;
  //handle as much as possible taht have no side effects. function could
  //return some MAP / SET -> TODO, but not likely
  if (['string', 'boolean', 'number', 'function'].includes(type)) {
    return JSON.stringify(value)
  } else if (Object.prototype.toString.call(value) === '[object Object]') {
    let parts = [];
    for (let key in value) {
      if (Object.prototype.hasOwnProperty.call(value, key)) {
        parts.push(JSON.stringify(key) + ': ' + value_to_json(value[key]));
      }
    }
    return '{' + parts.join(',') + '}';
  }
  else if (value instanceof Map) {
    let parts_in_order = [];
    value.forEach((entry, key) => {
      if (typeof key === 'string') {
        parts_in_order.push(JSON.stringify(key) + ':' + value_to_json(entry));
      } else {
        console.log('Non String KEYS in MAP not directly supported');
      }
      //FOR OTHER KEY TYPES ADD CUSTOM... 'Key' encoding...
    });
    return '{' + parts_in_order.join(',') + '}';
  } else if (typeof value[Symbol.iterator] !== "undefined") {
    //Other iterables like SET (also in ORDER)
    let parts = [];
    for (let entry of value) {
      parts.push(value_to_json(entry))
    }
    return '[' + parts.join(',') + ']';
  } else {
    return JSON.stringify(value)
  }
}


let m = new Map();
m.set('first', 'first_value');
m.set('second', 'second_value');
let m2 = new Map();
m2.set('nested', 'nested_value');
m.set('sub_map', m2);
let map_in_array = new Map();
map_in_array.set('key', 'value');
let set1 = new Set(["1", 2, 3.0, 4]);

m2.set('array_here', [map_in_array, "Hello", true, 0.1, null, undefined, Number.POSITIVE_INFINITY, {
  "a": 4
}]);
m2.set('a set: ', set1);
const test = {
  "hello": "ok",
  "map": m
};

console.log(value_to_json(test));
hobbit_be
  • 196
  • 1
  • 5
1

js use the localStorage API to store the ES6 Map

bug

"[object Map]" ❌


(() => {
  const map = new Map();
  map.set(1, {id: 1, name: 'eric'});
  // Map(1) {1 => {…}}
  // ❌
  localStorage.setItem('app', map);
  localStorage.getItem('app');
  // "[object Map]"
})();



solution

use JSON.stringify to serialize the Map object before storing it and then use JSON.parse to deserialize it before access the the Map object ✅



(() => {
  const map = new Map();
  map.set(1, {id: 1, name: 'eric'});
  // Map(1) {1 => {…}}
  // ✅
  localStorage.setItem('app', JSON.stringify([...map]));
  const newMap = new Map(JSON.parse(localStorage.getItem('app')));
  // Map(1) {1 => {…}}
})();

screenshots

enter image description here

refs

https://www.cnblogs.com/xgqfrms/p/14431425.html

xgqfrms
  • 10,077
  • 1
  • 69
  • 68
0

It's important to remember that if you try to setItem on a huge map collection, it will throw Quota Exceeded Error. I tried persisting to local storage a map with 168590 entries and got this error. :(

KanwarG
  • 1,024
  • 2
  • 11
  • 18