170

How would one JSON.stringify() a Set?

Things that did not work in Chromium 43:

var s = new Set(['foo', 'bar']);

JSON.stringify(s); // -> "{}"
JSON.stringify(s.values()); // -> "{}"
JSON.stringify(s.keys()); // -> "{}"

I would expect to get something similar to that of a serialized array.

JSON.stringify(["foo", "bar"]); // -> "["foo","bar"]"
MitMaro
  • 5,607
  • 6
  • 28
  • 52
  • 2
    closely related: [How do I persist a ES6 Map in localstorage (or elsewhere)?](http://stackoverflow.com/q/28918232/1048572) – Bergi Jul 02 '15 at 20:06

4 Answers4

190

JSON.stringify doesn't directly work with sets because the data stored in the set is not stored as properties.

But you can convert the set to an array. Then you will be able to stringify it properly.

Any of the following will do the trick:

JSON.stringify([...s]);
JSON.stringify([...s.keys()]);
JSON.stringify([...s.values()]);
JSON.stringify(Array.from(s));
JSON.stringify(Array.from(s.keys()));
JSON.stringify(Array.from(s.values()));
Oriol
  • 274,082
  • 63
  • 437
  • 513
  • 3
    I was gonna propose `Array.from()`. But this looks idiomically better. – TaoPR Jul 02 '15 at 17:26
  • 5
    Just to add some reference [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) has some examples of this and other related techniques – Amit Jul 02 '15 at 17:29
  • 5
    A list comprehension could work as well: `JSON.stringify([_ for (_ of s)])` – Whymarrh Jul 02 '15 at 17:34
  • 1
    All great ways of doing it, sadly they don't work in Chromium 43 out of the box as spread and Array.from are not yet implemented. Looks like Babel to the rescue. – MitMaro Jul 02 '15 at 17:43
  • 1
    @MitMaro You can enable chrome://flags/#enable-javascript-harmony – Oriol Jul 02 '15 at 17:47
  • 2
    A current proposal to improve the default of `Set.prototype.toJSON`: https://github.com/DavidBruant/Map-Set.prototype.toJSON – Thom4 Sep 16 '15 at 13:50
  • 1
    I'm so pleased with the order in which you presented those lines of code: sorted mostly by length, but grouping the `...` approaches together. Perfect! – Wyck Jan 20 '21 at 05:59
  • But then you lose all the advantages of using a set in first place, it just converts it to array. – eliezra236 Jan 01 '22 at 16:58
  • This works if you're trying to stringify a `Set` directly. However, if the `Set` is not "top level", then you're better off using [replacers as explained by tanguy_k](https://stackoverflow.com/a/46491780/114558). – rinogo Feb 05 '22 at 00:44
78

You can pass a "replacer" function to JSON.stringify:

const fooBar = {
  foo: new Set([1, 2, 3]),
  bar: new Set([4, 5, 6])
};

JSON.stringify(
  fooBar,
  (_key, value) => (value instanceof Set ? [...value] : value)
);

Result:

"{"foo":[1,2,3],"bar":[4,5,6]}"

toJSON is a legacy artifact, and a better approach is to use a custom replacer, see https://github.com/DavidBruant/Map-Set.prototype.toJSON/issues/16

tanguy_k
  • 11,307
  • 6
  • 54
  • 58
  • 14
    there is shorten version in ES6 `JSON.stringify(fooBar, (key, value) => value instanceof Set ? [...value] : value)` – OzzyCzech Nov 29 '19 at 10:25
  • 6
    The corresponding reviver is left as an exercise for the reader? ;-) – Robert Siemer May 31 '20 at 21:29
  • 1
    Will you please incorporate the comment from @OzzyCzech into the answer? It's a fantastic modern solution. – rinogo Feb 05 '22 at 00:47
  • This is a very nice way of dealing with deep sets to save more nasty iterations. Well done – Andross Mar 05 '22 at 02:19
  • I'm not sure why this is so popular, when you can't get Sets back easily from this? If you parse that result and try `parsed.foo.add(1)` ... it dies, because an array does't have an `add` function. – GreenAsJade Jul 25 '22 at 12:31
  • For anyone else coming here later wondering how to get a Set back from this, `JSON.parse` takes an equivalent function called `reviver` that's run on everything it parses. I ended up solving this by adding `"__isSet"` to the start of any array made out of a set and then checking for this in the reviver function, turning it back into a set when found. – LukeZaz Apr 26 '23 at 00:29
  • For reviver (& Map), see https://stackoverflow.com/a/56150320/9549068 – Nor.Z Jul 11 '23 at 09:03
13

While all of the above work I suggest that you subclass set and add a toJSON method to make sure that it stringify's correctly. Especially if you are going to be stringifying often. I use sets in my Redux stores and needed to make sure this was never a problem.

This is a basic implementation. Naming is just to illustrate the point pick your own style.

class JSONSet extends Set {
    toJSON () {
        return [...this]
    }
}

const set = new JSONSet([1, 2, 3])
console.log(JSON.stringify(set))
Stephen Bolton
  • 365
  • 2
  • 10
  • 8
    I wouldn't recommend this approach as `toJSON` is considered a legacy feature. A proposal to add `toJSON` to both `set` and `map` was rejected: https://github.com/DavidBruant/Map-Set.prototype.toJSON/issues/16 – MitMaro Jul 18 '17 at 11:15
  • Overriding `constructor` is not necessary. – Justin Johnson Mar 15 '18 at 09:23
3

The problem with all the previous approaches is that they all convert the set into Array, which is missing the entire point of Set and indexes.

What you should do is to use an Object instead. Either convert it with the following function or simply create it as Object instead of Set.

const mySet = new Set(['hello', 'world']);
const myObj = {};
for (let value of mySet.values()) {
  myObj[value] = true;
}

Then instead of using mySet.has('hello') Do myObj.hasOwnProperty('hello').

Then stringify it as an object without a problem.

Note: The following method uses more memory because it needs to store the value as well as the key. But performence wise it's still O(1) compared to Array.includes() which is O(n) and miss the point of even using a Set.

eliezra236
  • 547
  • 1
  • 4
  • 16