2

Is there an existing javascript library which will deserialize Json.Net with reference loop handling?

{
    "$id": "1",
    "AppViewColumns": [
        {
            "$id": "2",
            "AppView": {"$ref":"1"},
            "ColumnID": 1,
        }
    ]
}

this should deserialize to an object with a reference loop between the object in the array and the outer object

DrSammyD
  • 880
  • 12
  • 32

3 Answers3

4

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);
}
Josh Mouch
  • 3,480
  • 1
  • 37
  • 34
  • By array references, do you mean `"$values"`? I had that problem, and it seems to be fixed by checking if `p == "$values"` and then replacing `s` with the relink, rather than updating `s[p]` (which is `s["$values"]`).. if that makes sense. – Jeppe Jun 21 '20 at 18:21
1

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.

user2864740
  • 60,010
  • 15
  • 145
  • 220
  • I suppose we'd also need a re-serializer as well... I might make it a plugin for lodash/underscore – DrSammyD Feb 10 '14 at 20:22
  • 1
    @DrSammyD If you wish to go back the other way, then yes. But - *maybe* you don't need this extra data in *both* directions? – user2864740 Feb 10 '14 at 20:23
  • I just created a jsfiddle which goes both ways. Care to include it in your answer? http://jsfiddle.net/8hLCw/13/ – DrSammyD Feb 10 '14 at 21:10
  • @DrSammyD Feel free to edit the answer (or add your own) as you feel. I've included the fiddle link so as to make it more readily available. – user2864740 Feb 10 '14 at 21:21
  • This won't work if the object with the id is parsed after the object with a reference to that id – DrSammyD Feb 10 '14 at 23:02
  • 1
    This didn't work for me with the latest version of JSON.Net in dnx. It uses both "$id" and "$ref", and they may be out of order. I've modified your answer. – Josh Mouch Feb 04 '16 at 23:00
1

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

DrSammyD
  • 880
  • 12
  • 32