JSON with Maps

Benchmark created on


Setup

let basic_obj = {
  a: true,
  b: ["a", "b", "cdef"],
  c: {
    d: new Map([
      ["a", 1],
      ["b", 1],
    ]),
  },
};

let tricky_obj = {
  a: new Map([
    ["@Array", 1],
    [{ _meta: "@Array" }, 1],
  ]),
  "@Set": new Set([[{ _meta: 1 }], [{ _meta: 2 }]]),
};

let long_array = ["@Array"];
for(let i = 0; i<10000; i++) {
	long_array.push(i);
}

function make_large_object(c = 4) {
  if (c <= 0) {
	return Math.random();
  }
  // Large arrays
  // Large nested objects
  let variants = [
    "object",
    "evil-object",
    "array",
    "evil-array",
    "map",
    "number",
    "string",
  ];
  let variant = variants[Math.floor(Math.random() * variants.length)];

  if (variant == "object" || variant == "evil-object") {
    let size = Math.floor(Math.random() * 10);
    let obj = {};
    if (variant == "evil-object") {
      obj["_meta"] = 3;
    }
    for (let i = 0; i < size; i++) {
      obj[make_large_object(c - 1)] = make_large_object(c - 1);
    }
    return obj;
  } else if (variant == "array" || variant == "evil-array") {
    let size = Math.floor(Math.random() * 500);
    let arr = [];
    if (variant == "evil-array") {
      arr.push("@Map");
    }
    for (let i = 0; i < size; i++) {
      arr.push(make_large_object(c - 3));
    }
    return arr;
  } else if (variant == "map") {
    let size = Math.floor(Math.random() * 100);
    let map = new Map();
    for (let i = 0; i < size; i++) {
      map.set(make_large_object(c - 2), make_large_object(c - 2));
    }
    return map;
  } else if (variant == "number") {
    return Math.random();
  } else if (variant == "string") {
    return Math.random().toString(36).substring(7);
  } else {
    throw "Unknown variant";
  }
}

let large_objects = [];
for (let i = 0; i < 10; i++) {
  large_objects.push(make_large_object());
}

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_[0] === "@Undefined")
    ) {
      value_ = value_.slice();
      value_.unshift("@Array");
    }
  } else if (value_ === undefined) value_ = ["@Undefined"];

  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 (typeof value_ === "object") {
    if (Array.isArray(value_) && value_.length > 0) {
      var 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_);
      } else if (value_[0] === "@Undefined") value_ = undefined;
    }
  }

  return value_;
}

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

Test runner

Ready to run.

Testing in
TestOps/sec
Array Based
let result = JSON.parse(JSON.stringify(basic_obj,jsonMapSetReplacer ),jsonMapSetReviver);
ready
Map Based
let result = JSON.parse(JSON.stringify(basic_obj,stringifyReplacer),parseReviver);
ready
Array Based
let result = JSON.parse(JSON.stringify(tricky_obj,jsonMapSetReplacer ),jsonMapSetReviver);
ready
Map Based
let result = JSON.parse(JSON.stringify(tricky_obj,stringifyReplacer),parseReviver);
ready
Array Based
for(let i = 0; i < large_objects.length; i++) {
  let result = JSON.parse(JSON.stringify(large_objects[i],jsonMapSetReplacer ),jsonMapSetReviver);
}
ready
Map Based
for(let i = 0; i < large_objects.length; i++) {
  let result = JSON.parse(JSON.stringify(large_objects[i],stringifyReplacer),parseReviver);
}
ready
Array Based
let result = JSON.parse(JSON.stringify(long_array,jsonMapSetReplacer ),jsonMapSetReviver);
ready
Map Based
let result = JSON.parse(JSON.stringify(long_array,stringifyReplacer),parseReviver);
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.