JSON with Maps (v7)

Revision 7 of this 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_ = value_.slice();
      value_.unshift("@Array");
    }
  }

  return value_;
}

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

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

function jsonMapSetReplacer2 (_, 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_ = value_.slice ();
      value_.unshift ('@Array');
    }
  }

  return value_;
}

function jsonMapSetReviver2 (_, value_) {
  if (Array.isArray (value_) && value_.length > 0) {
    let 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_);
    }
  }

  return value_;
}

function jsonMapSetReplacer3 (_, value_) {
  if (typeof (value_) === 'object') {
    if (value_ instanceof Map) {
      value_ = Array.from (value_);
      value_.push ('@Map');
    }
    else if (value_ instanceof Set) {
      value_ = Array.from (value_);
      value_.push ('@Set');
    }
    else if (Array.isArray (value_) && value_.length > 0) {
      let last = value_ [value_.length - 1];
      if (last === '@Map' || last === '@Set' || last === '@Array') {
        value_ = value_.slice ();
        value_.push ('@Array');
      }
    }
  }

  return value_;
}

function jsonMapSetReviver3 (_, value_) {
  if (Array.isArray (value_) && value_.length > 0) {
    let isMap, isSet, last = value_ [value_.length - 1];
    if ((isMap = last === '@Map') || (isSet = last === '@Set') || last === '@Array') {
      value_.pop ();
      if (isMap)
        value_ = new Map (value_);
      else if (isSet)
        value_ = new Set (value_);
    }
  }

  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
Array Based 3
let result = JSON.parse(JSON.stringify(basic_obj,jsonMapSetReplacer3),jsonMapSetReviver3);
ready
Array Based 3
let result = JSON.parse(JSON.stringify(tricky_obj,jsonMapSetReplacer3),jsonMapSetReviver3);
ready
Array Based 3
for(let i = 0; i < large_objects.length; i++) {
  let result = JSON.parse(JSON.stringify(large_objects[i],jsonMapSetReplacer3),jsonMapSetReviver3);
}
ready
Array Based 3
let result = JSON.parse(JSON.stringify(long_array,jsonMapSetReplacer3),jsonMapSetReviver3);
ready

Revisions

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