Resolve circular references from JSON object
Asked Answered
P

6

16

If I have a serialized JSON from json.net like so:

User:{id:1,{Foo{id:1,prop:1}},
FooList{$ref: "1",Foo{id:2,prop:13}}

I want to have knockout output a foreach over FooList but I am not sure how to proceed because the $ref things could throw things.

I'm thinking the solution would be to somehow force all the Foos to be rendered in the FooList by not using:

PreserveReferencesHandling = PreserveReferencesHandling.Objects

but that seems wasteful..

Phylys answered 9/3, 2013 at 15:40 Comment(2)
Another solution for this: #10747841Addax
See also JsonNetDecycleSafety
A
14

The json object which you are receiving from the server contains Circular References. Before using the object you should have to first remove all the $ref properties from the object, means in place of $ref : "1" you have to put the object which this link points.

In your case may be it is pointing to the User's object whose id is 1

For this you should check out Douglas Crockfords Plugin on github.There is a cycle.js which can do the job for you.

or you can use the following code (not tested) :

function resolveReferences(json) {
    if (typeof json === 'string')
        json = JSON.parse(json);

    var byid = {}, // all objects by id
        refs = []; // references to objects that could not be resolved
    json = (function recurse(obj, prop, parent) {
        if (typeof obj !== 'object' || !obj) // a primitive value
            return obj;
        if ("$ref" in obj) { // a reference
            var ref = obj.$ref;
            if (ref in byid)
                return byid[ref];
            // else we have to make it lazy:
            refs.push([parent, prop, ref]);
            return;
        } else if ("$id" in obj) {
            var id = obj.$id;
            delete obj.$id;
            if ("$values" in obj) // an array
                obj = obj.$values.map(recurse);
            else // a plain object
                for (var prop in obj)
                    obj[prop] = recurse(obj[prop], prop, obj)
            byid[id] = obj;
        }
        return obj;
    })(json); // run it!

    for (var i=0; i<refs.length; i++) { // resolve previously unknown references
        var ref = refs[i];
        ref[0][ref[1]] = byid[refs[2]];
        // Notice that this throws if you put in a reference at top-level
    }
    return json;
}  

Let me know if it helps !

Anceline answered 11/3, 2013 at 8:19 Comment(1)
If you move the byid[id] = obj assignment up (behind the var id =... assignment), you get much less entries in the refs array. In my objects graphs, I got none at all.Porett
G
29

I've found some bugs and implemented arrays support:

function resolveReferences(json) {
    if (typeof json === 'string')
        json = JSON.parse(json);

    var byid = {}, // all objects by id
        refs = []; // references to objects that could not be resolved
    json = (function recurse(obj, prop, parent) {
        if (typeof obj !== 'object' || !obj) // a primitive value
            return obj;
        if (Object.prototype.toString.call(obj) === '[object Array]') {
            for (var i = 0; i < obj.length; i++)
                // check also if the array element is not a primitive value
                if (typeof obj[i] !== 'object' || !obj[i]) // a primitive value
                    continue;
                else if ("$ref" in obj[i])
                    obj[i] = recurse(obj[i], i, obj);
                else
                    obj[i] = recurse(obj[i], prop, obj);
            return obj;
        }
        if ("$ref" in obj) { // a reference
            var ref = obj.$ref;
            if (ref in byid)
                return byid[ref];
            // else we have to make it lazy:
            refs.push([parent, prop, ref]);
            return;
        } else if ("$id" in obj) {
            var id = obj.$id;
            delete obj.$id;
            if ("$values" in obj) // an array
                obj = obj.$values.map(recurse);
            else // a plain object
                for (var prop in obj)
                    obj[prop] = recurse(obj[prop], prop, obj);
            byid[id] = obj;
        }
        return obj;
    })(json); // run it!

    for (var i = 0; i < refs.length; i++) { // resolve previously unknown references
        var ref = refs[i];
        ref[0][ref[1]] = byid[ref[2]];
        // Notice that this throws if you put in a reference at top-level
    }
    return json;
}
Governor answered 2/4, 2013 at 6:25 Comment(5)
- Added if (Object.prototype.toString.call(obj) === '[object Array]') { ... } - Almost latest string was with mistake: ref[0][ref[1]] = byid[refs[2]]; but must be: ref[0][ref[1]] = byid[ref[2]]; - And this string was: obj[prop] = recurse(obj[prop], prop, obj) and becomes: obj[prop] = recurse(obj[prop], prop, obj);Governor
thanks a lot! i spend a lot of time for searching bug!Buskirk
Still there is a bug in Arrays processing, here you shouldn't return anything if it is primitive :::code::: if (typeof obj[i] !== 'object' || !obj[i]) return obj[i];Cecillececily
Would this work with "nested" arrays? I tried to no luck.Goldshell
exactly what i was looking forSvetlana
A
14

The json object which you are receiving from the server contains Circular References. Before using the object you should have to first remove all the $ref properties from the object, means in place of $ref : "1" you have to put the object which this link points.

In your case may be it is pointing to the User's object whose id is 1

For this you should check out Douglas Crockfords Plugin on github.There is a cycle.js which can do the job for you.

or you can use the following code (not tested) :

function resolveReferences(json) {
    if (typeof json === 'string')
        json = JSON.parse(json);

    var byid = {}, // all objects by id
        refs = []; // references to objects that could not be resolved
    json = (function recurse(obj, prop, parent) {
        if (typeof obj !== 'object' || !obj) // a primitive value
            return obj;
        if ("$ref" in obj) { // a reference
            var ref = obj.$ref;
            if (ref in byid)
                return byid[ref];
            // else we have to make it lazy:
            refs.push([parent, prop, ref]);
            return;
        } else if ("$id" in obj) {
            var id = obj.$id;
            delete obj.$id;
            if ("$values" in obj) // an array
                obj = obj.$values.map(recurse);
            else // a plain object
                for (var prop in obj)
                    obj[prop] = recurse(obj[prop], prop, obj)
            byid[id] = obj;
        }
        return obj;
    })(json); // run it!

    for (var i=0; i<refs.length; i++) { // resolve previously unknown references
        var ref = refs[i];
        ref[0][ref[1]] = byid[refs[2]];
        // Notice that this throws if you put in a reference at top-level
    }
    return json;
}  

Let me know if it helps !

Anceline answered 11/3, 2013 at 8:19 Comment(1)
If you move the byid[id] = obj assignment up (behind the var id =... assignment), you get much less entries in the refs array. In my objects graphs, I got none at all.Porett
G
14

This is actually extremely simple if you take advantage of JSON.parse's reviver parameter.

Example below. See browser console for the output because StackOverflow's snippet console output will not provide an accurate picture of what the result is.

// example JSON
var j = '{"$id":"0","name":"Parent",' +
            '"child":{"$id":"1", "name":"Child","parent":{"$ref":"0"}},' + 
            '"nullValue":null}';

function parseAndResolve(json) {
    var refMap = {};

    return JSON.parse(json, function (key, value) {
        if (key === '$id') { 
            refMap[value] = this;
            // return undefined so that the property is deleted
            return void(0);
        }

        if (value && value.$ref) { return refMap[value.$ref]; }

        return value; 
    });
}

console.log(parseAndResolve(j));
<b>See actual browser console for output.</b>
Guild answered 22/2, 2017 at 17:53 Comment(9)
Hi, this is working great but I've got an error with object field set to null. replace "if (value.$ref)" by "if (value && value.$ref)" resolve this :-)Toothache
@Toothache Thank you for figuring that out and letting me know! Answer updated.Guild
This one is great!Azeria
how would you fit it into getting replies from http get (eg from REST service) ? The get fails and the typescript code is not called if that..Medievalist
@BoppityBop I think the answer to that would depend on why the GET is failing. Why is it failing?Guild
because it throws = circular ref in json.. duhMedievalist
@BoppityBop What throws = circular ref? Where? In your JS code? On the server? I think your situation is more involved than something that can be resolved in the comments and needs a proper description of the problem. I think you're probably best off asking a new question here. If you link me to it, I can have a look.Guild
I stopped to care about it - I switched to use newtonsoft json instead of aspnet system.text json and it works (so far :))... but the exception was thrown somewhere in http pipe - before my code can get to it and fix in the way you are proposing it.. unfortunately.. I mean someone very clever might probably override http interceptors in angular and do something about it but I am sure it will be hell to support.. sorry ignore my comments..Medievalist
@BoppityBop It sounds like you are using a query library that automatically parses the response as JSON instead of giving you the text, and there may be some way to ask it to just give you the text, which would allow you to use my approach above. Or it may have some kind of hooks interface that would allow you to override the built-in parsing logic.Guild
I
3

I had trouble with the array correction in the answer of Alexander Vasiliev.

I can't comment his answer (don't own enough reputations points ;-) ), so I had to add a new answer... (where I had a popup as best practice not to answer on other answers and only on the original question - bof)

    if (Object.prototype.toString.call(obj) === '[object Array]') {
        for (var i = 0; i < obj.length; i++) {
            // check also if the array element is not a primitive value
            if (typeof obj[i] !== 'object' || !obj[i]) // a primitive value
                return obj[i];
            if ("$ref" in obj[i])
                obj[i] = recurse(obj[i], i, obj);
            else
                obj[i] = recurse(obj[i], prop, obj);
        }
        return obj;
    }
Ivonne answered 18/11, 2013 at 15:4 Comment(2)
However I do not use it anymore in production as latest version of Microsoft ASP.NET OData server-side implementation doesn't support output with "$ref" to reference already returned objects. And Microsoft says in its forums that they will not implement it. ;-(Governor
Is it necessary to make the $ref distinction in the array loop? The next instance will do the check anyway, and in case of a non-ref, I doubt that the passing of 'prop' as second parameter is correct.Porett
A
2

In the accepted implementation, if you're inspecting an array and come across a primitive value, you will return that value and overwrite that array. You want to instead continue inspecting all of the elements of the array and return the array at the end.

function resolveReferences(json) {
    if (typeof json === 'string')
        json = JSON.parse(json);

    var byid = {}, // all objects by id
        refs = []; // references to objects that could not be resolved
    json = (function recurse(obj, prop, parent) {
        if (typeof obj !== 'object' || !obj) // a primitive value
            return obj;
        if (Object.prototype.toString.call(obj) === '[object Array]') {
            for (var i = 0; i < obj.length; i++)
                // check also if the array element is not a primitive value
                if (typeof obj[i] !== 'object' || !obj[i]) // a primitive value
                    continue;
                else if ("$ref" in obj[i])
                    obj[i] = recurse(obj[i], i, obj);
                else
                    obj[i] = recurse(obj[i], prop, obj);
            return obj;
        }
        if ("$ref" in obj) { // a reference
            var ref = obj.$ref;
            if (ref in byid)
                return byid[ref];
            // else we have to make it lazy:
            refs.push([parent, prop, ref]);
            return;
        } else if ("$id" in obj) {
            var id = obj.$id;
            delete obj.$id;
            if ("$values" in obj) // an array
                obj = obj.$values.map(recurse);
            else // a plain object
                for (var prop in obj)
                    obj[prop] = recurse(obj[prop], prop, obj);
            byid[id] = obj;
        }
        return obj;
    })(json); // run it!

    for (var i = 0; i < refs.length; i++) { // resolve previously unknown references
        var ref = refs[i];
        ref[0][ref[1]] = byid[ref[2]];
        // Notice that this throws if you put in a reference at top-level
    }
    return json;
}
Asante answered 21/5, 2015 at 21:2 Comment(0)
D
1

my solution(works for arrays as well):

usage: rebuildJsonDotNetObj(jsonDotNetResponse)

The code:

function rebuildJsonDotNetObj(obj) {
    var arr = [];
    buildRefArray(obj, arr);
    return setReferences(obj, arr)
}

function buildRefArray(obj, arr) {
    if (!obj || obj['$ref'])
        return;
    var objId = obj['$id'];
    if (!objId)
    {
        obj['$id'] = "x";
        return;
    }
    var id = parseInt(objId);
    var array = obj['$values'];
    if (array && Array.isArray(array)) {
        arr[id] = array;
        array.forEach(function (elem) {
            if (typeof elem === "object")
                buildRefArray(elem, arr);
        });
    }
    else {
        arr[id] = obj;
        for (var prop in obj) {
            if (typeof obj[prop] === "object") {
                buildRefArray(obj[prop], arr);
            }
        }
    }
}

function setReferences(obj, arrRefs) {
    if (!obj)
        return obj;
    var ref = obj['$ref'];
    if (ref)
        return arrRefs[parseInt(ref)];

    if (!obj['$id']) //already visited
        return obj;

    var array = obj['$values'];
    if (array && Array.isArray(array)) {
        for (var i = 0; i < array.length; ++i)
            array[i] = setReferences(array[i], arrRefs)
        return array;
    }
    for (var prop in obj)
        if (typeof obj[prop] === "object")
            obj[prop] = setReferences(obj[prop], arrRefs)
    delete obj['$id'];
    return obj;
}
Dolorous answered 5/2, 2016 at 0:35 Comment(1)
+1 this works great for output produced by System.Text.Json as well. I had to call the function as follows rebuildJsonDotNetObj(JSON.parse(myjson)) where the myjson variable holds the JSON text.Marriage

© 2022 - 2024 — McMap. All rights reserved.