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