-1

When we call JSON.stringify on a Map or Set, we get empty data:

console.log(JSON.stringify({foo: new Map([[1,2], [3,4]])}));

we just get:

{"foo":{}}

I can more or less fix the above by using:

console.log(JSON.stringify({foo: Array.from(new Map([[1,2], [3,4]]))}));

but my question is - are there any other core JS classes that don't get serialized automatically, besides Map and Set?

  • 2
    Date objects come to mind. And Symbol. – Pointy Sep 30 '19 at 21:10
  • For sure, so dates don't get converted to strings? –  Sep 30 '19 at 21:10
  • if it's a Date, I get: `{"foo":"2019-09-30T21:10:43.709Z"}`, seems ok? –  Sep 30 '19 at 21:10
  • 2
    Well yes but they're not represented in any special way; they're just strings in the JSON output so when you parse the JSON you still just have the string. – Pointy Sep 30 '19 at 21:11
  • I see, that makes sense, but that's' true of all classes, everything becomes an object literal or primitive or whatever –  Sep 30 '19 at 21:11
  • If you think about it, serializing a Map or a Set to JSON would be pretty close to impossible in the general case. – Pointy Sep 30 '19 at 21:12
  • Is the question asking for a complete list of all the native prototypes that don't play nicely with stringify? Seems overly broad. – Taplar Sep 30 '19 at 21:12
  • 1
    It seems like the distinction the question is making is classes that produce useful vs. useless JSON serialization. – Barmar Sep 30 '19 at 21:12
  • Any DOM-related object. `ImageData`. Any exotic object, really. BigInts would even throw an error. – Sebastian Simon Sep 30 '19 at 21:12
  • 1
    From this [answer](https://stackoverflow.com/a/122704/2924577): `Date`s, functions, `undefined`, `Infinity`, RegExps, Maps, Sets, Blobs, FileLists, ImageDatas, sparse Arrays, Typed Arrays, and other complex types. – Nikhil Sep 30 '19 at 21:13
  • I updated the OP, `Array.from(new Map())` or `Array.from(net Set())` works fine most of the time. It's more about which other classes I should handle. –  Sep 30 '19 at 21:13
  • `Array.from(new Map(...))` preserves the data, but not the mapping relationship. But `Map` allows things as keys that JSON doesn't, so that seems to be the best you can do, the receiver would need to convert it back. – Barmar Sep 30 '19 at 21:17
  • @Barmar it preserves the mapping relationship as long as you know to call `new Map(Array.from(originalMap))` right? –  Sep 30 '19 at 21:18
  • Basically, if you check the JSON specification, and there's no obvious translation from a JS type to a JSON type, it won't work. – Barmar Sep 30 '19 at 21:18
  • 1
    Yes, that's what I meant about the receiver needing to know to convert it back. Much of the value of JSON is that the translation to a language type is automatic. – Barmar Sep 30 '19 at 21:19
  • is there a way to convert a sparse array to a simple object? –  Sep 30 '19 at 21:19
  • If you get rid of the `length` property it might be treated as an ordinary object whose keys are the indexes. – Barmar Sep 30 '19 at 21:20
  • Hmm, that doesn't seem to work. I tried `a = [1, 2,,,3]; delete a.length;` but the property didn't go away. – Barmar Sep 30 '19 at 21:21
  • intetrestingly, `Buffer.from` serializes to `{"foo":{"type":"Buffer","data":[1,2,3]}}` –  Sep 30 '19 at 21:22
  • `Date` automatically serializing as a formatted date-time string is an exception to my rule of thumb. – Barmar Sep 30 '19 at 21:23
  • MDN has a nice section on supported types [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description). You can find a list of js builtins [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects) and test the ones you're interested in as well. The most important thing to note is that, for Objects, unless they have a `toJSON` method, they will simply list their own enumerable properties. `Date` implements `toJSON` so that's why it returns a date string whereas `Map` and `Set` do not. – Khauri Sep 30 '19 at 21:29
  • @Barmar An array's `.length` cannot be `delete`d. – Bergi Sep 30 '19 at 21:33
  • JSON only supports: strings, numbers, booleans, null and arrays and objects with those as values/properties. That's it. So, no other type of object that is comprised of anything more than that. You have to either manually convert your object to something that can be represented with these basic types in JSON or find some other way to represent your object. The data within an object like a `Map` with simple values/indexes could be manually put into JSON form (perhaps in an array of objects or an array of arrays), but converting it back into a Map when deserialized will have to be done manually. – jfriend00 Sep 30 '19 at 22:47
  • For what goes in JSON, see: https://www.json.org/ – jfriend00 Sep 30 '19 at 22:50

1 Answers1

0

JSON only supports: strings, numbers, booleans, null and arrays and plain objects with those as values/properties. That's it. So, no other type of object that is comprised of anything more than that is directly supported by JSON.

You have to either manually convert your object to something that can be represented with these basic types in JSON or find some other way to represent your object. The data within an object like a Map with simple values/indexes could be manually put into JSON form (perhaps in an array of objects or an array of arrays), but converting it back into a Map when deserialized will have to be done manually.

You can see the types that JSON supports here: https://www.json.org/

This means that no custom object type, even if its instance data is only composed of the above types will deserialize into the proper object type. This would include even something like a Date or a Regex and certainly includes any non-plain object such as Set, or Map.

It is possible to manually convert your data into something that is JSON serializable and then manually deserialize into the appropriate object. For example, you could serialize a non-nested Map and Set that only contains legal keys and values like this:

function serialize(mapOrSet) {
    if (mapOrSet instanceof Map) {
         // convert Map into 2D array
         // [[key1, val1], [key2, val2], ....]
         let data = [];
         for (const [key, value] of mapOrSet) {
             data.push([key, value]);
         }
         return JSON.stringify({
             _customType: "Map",
             _data: data
         });
    } else if (mapOrSet instanceof Set) {
         return JSON.stringify({
             _customType: "Set",
             _data: Array.from(mapOrSet);
         }); 
    } else {
         throw new TypeError("argument to serialize() must be Map or Set");
    }
}

Then, you'd have to have a custom deserializer that could detect objects with the _customType property on them and call an appropriate deserializer function to convert the plain JSON into the appropriate custom object types. You could even make the system extensible by having objects support their own serializing and deserializing functions, using some sort of _customType property to identify the appropriate class name to invoke. Of course, both source and destination would have to have the appropriate serialize and deserialize extensions present to make it work properly.

jfriend00
  • 683,504
  • 96
  • 985
  • 979