function deflate(obj) { function _deflate(obj, _sofar = new Map(), _cur_ref = [0]) { if (obj === null || typeof obj !== 'object') { return obj; } if (Array.isArray(obj)) { return obj.map(v => _deflate(v, _sofar, _cur_ref)); } const id = obj['#id'] || Symbol('id'); obj['#id'] = id; if (!_sofar.has(id)) { _sofar.set(id, {}); } else { const flat = _sofar.get(id); if (flat["#"] === undefined) { _cur_ref[0] += 1; flat["#"] = _cur_ref[0]; } return {"#": flat["#"]}; } const cleaned = Object.fromEntries(Object.entries(obj).map(([k, v]) => [(k.match(/^#+$/) ? '#' + k : k), _deflate(v, _sofar, _cur_ref)])); Object.assign(_sofar.get(id), cleaned); return _sofar.get(id); } return _deflate(obj); } function inflate(obj) { function _inflate(obj, _refs = {}) { if (obj === null || typeof obj !== 'object') { return obj; } if (Array.isArray(obj)) { return obj.map(v => _inflate(v, _refs)); } let ref_id = null; if ("#" in obj) { ref_id = obj["#"]; if (!(_refs[ref_id])) { _refs[ref_id] = {}; } } const cleaned = Object.fromEntries(Object.entries(obj).filter(([k, v]) => k !== "#").map(([k, v]) => [(k.startsWith('#') ? k.substring(1) : k), _inflate(v, _refs)])); if (ref_id === null) { return cleaned; } Object.assign(_refs[ref_id], cleaned); return _refs[ref_id]; } return _inflate(obj); } function test() { const a = {"#": "collide", "##": "collide"}; a["b"] = a; a["c"] = [a, a]; a["d"] = { "e": a, "f": [a, a] }; a["g"] = { "h": { "i": a, "j": [a, a] } }; const a2 = inflate(JSON.parse(JSON.stringify(deflate(a), null, 2))); console.assert(JSON.stringify(a) === JSON.stringify(a2), 'Test failed'); } test();