How to restore circular references (e.g. "$id") from Json.NET-serialized JSON?

Solution 1:

The answers given almost worked for me, but the latest version of MVC, JSON.Net, and DNX uses "$ref" and "$id", and they may be out of order. So I've modified user2864740's answer.

I should note that this code does not handle array references, which are also possible.

function RestoreJsonNetReferences(g) {
	var ids = {};

	function getIds(s) {
		// we care naught about primitives
		if (s === null || typeof s !== "object") { return s; }

		var id = s['$id'];
		if (typeof id != "undefined") {
			delete s['$id'];

			// either return previously known object, or
			// remember this object linking for later
			if (ids[id]) {
				throw "Duplicate ID " + id + "found.";
			}
			ids[id] = s;
		}

		// then, recursively for each key/index, relink the sub-graph
		if (s.hasOwnProperty('length')) {
			// array or array-like; a different guard may be more appropriate
			for (var i = 0; i < s.length; i++) {
				getIds(s[i]);
			}
		} else {
			// other objects
			for (var p in s) {
				if (s.hasOwnProperty(p)) {
					getIds(s[p]);
				}
			}
		}
	}

	function relink(s) {
		// we care naught about primitives
		if (s === null || typeof s !== "object") { return s; }

		var id = s['$ref'];
		delete s['$ref'];

		// either return previously known object, or
		// remember this object linking for later
		if (typeof id != "undefined") {
			return ids[id];
		}

		// then, recursively for each key/index, relink the sub-graph
		if (s.hasOwnProperty('length')) {
			// array or array-like; a different guard may be more appropriate
			for (var i = 0; i < s.length; i++) {
				s[i] = relink(s[i]);
			}
		} else {
			// other objects
			for (var p in s) {
				if (s.hasOwnProperty(p)) {
					s[p] = relink(s[p]);
				}
			}
		}

		return s;
	}

	getIds(g);
	return relink(g);
}

Solution 2:

I'm not aware of existing libraries with such support, but one could use the standard JSON.parse method and then manually walk the result restoring the circular references - it'd just be a simple store/lookup based on the $id property. (A similar approach can be used for reversing the process.)

Here is some sample code that uses such an approach. This code assumes the JSON has already been parsed to the relevant JS object graph - it also modifies the supplied data. YMMV.

function restoreJsonNetCR(g) {
  var ids = {};

  function relink (s) {
    // we care naught about primitives
    if (s === null || typeof s !== "object") { return s; }

    var id = s['$id'];
    delete s['$id'];

    // either return previously known object, or
    // remember this object linking for later
    if (ids[id]) {
      return ids[id];
    }
    ids[id] = s;

    // then, recursively for each key/index, relink the sub-graph
    if (s.hasOwnProperty('length')) {
      // array or array-like; a different guard may be more appropriate
      for (var i = 0; i < s.length; i++) {
        s[i] = relink(s[i]);
      }
    } else {
      // other objects
      for (var p in s) {
        if (s.hasOwnProperty(p)) {
          s[p] = relink(s[p]);
        }
      }
    }

    return s;
  }

  return relink(g);
}

And the usage

var d = {
    "$id": "1",
    "AppViewColumns": [
        {
            "$id": "2",
            "AppView": {"$id":"1"},
            "ColumnID": 1,
        }
    ]
};

d = restoreJsonNetCR(d);
// the following works well in Chrome, YMMV in other developer tools
console.log(d);

DrSammyD created an underscore plugin variant with round-trip support.

Solution 3:

Ok so I created a more robust method which will use $id as well as $ref, because that's actually how json.net handles circular references. Also you have to get your references after the id has been registered otherwise it won't find the object that's been referenced, so I also have to hold the objects that are requesting the reference, along with the property they want to set and the id they are requesting.

This is heavily lodash/underscore based

(function (factory) {
    'use strict';
    if (typeof define === 'function' && define.amd) {
        define(['lodash'], factory);
    } else {
        factory(_);
    }
})(function (_) {
    var opts = {
        refProp: '$ref',
        idProp: '$id',
        clone: true
    };
    _.mixin({
        relink: function (obj, optsParam) {
            var options = optsParam !== undefined ? optsParam : {};
            _.defaults(options, _.relink.prototype.opts);
            obj = options.clone ? _.clone(obj, true) : obj;
            var ids = {};
            var refs = [];
            function rl(s) {
                // we care naught about primitives
                if (!_.isObject(s)) {
                    return s;
                }
                if (s[options.refProp]) {
                    return null;
                }
                if (s[options.idProp] === 0 || s[options.idProp]) {
                    ids[s[options.idProp]] = s;
                }
                delete s[options.idProp];
                _(s).pairs().each(function (pair) {
                    if (pair[1]) {
                        s[pair[0]] = rl(pair[1]);
                        if (s[pair[0]] === null) {
                            if (pair[1][options.refProp] !== undefined) {
                                refs.push({ 'parent': s, 'prop': pair[0], 'ref': pair[1][options.refProp] });
                            }
                        }
                    }
                });

                return s;
            }

            var partialLink = rl(obj);
            _(refs).each(function (recordedRef) {
                recordedRef['parent'][recordedRef['prop']] = ids[recordedRef['ref']] || {};
            });
            return partialLink;
        },
        resolve: function (obj, optsParam) {
            var options = optsParam !== undefined ? optsParam : {};
            _.defaults(options, _.resolve.prototype.opts);
            obj = options.clone ? _.clone(obj, true) : obj;
            var objs = [{}];

            function rs(s) {
                // we care naught about primitives
                if (!_.isObject(s)) {
                    return s;
                }
                var replacementObj = {};

                if (objs.indexOf(s) != -1) {
                    replacementObj[options.refProp] = objs.indexOf(s);
                    return replacementObj;
                }
                objs.push(s);
                s[options.idProp] = objs.indexOf(s);
                _(s).pairs().each(function (pair) {
                    s[pair[0]] = rs(pair[1]);
                });

                return s;
            }

            return rs(obj);
        }
    });
    _(_.resolve.prototype).assign({ opts: opts });
    _(_.relink.prototype).assign({ opts: opts });
});

I created a gist here