15

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..

Gaurav
  • 8,367
  • 14
  • 55
  • 90
FutuToad
  • 2,750
  • 5
  • 36
  • 63
  • Another solution for this: http://stackoverflow.com/questions/10747341/is-there-a-jquery-function-that-can-take-a-ref-id-value-from-a-parsed-json-stri/12622112#12622112 – jvitor83 Apr 15 '13 at 16:54
  • See also [JsonNetDecycle](https://bitbucket.org/smithkl42/jsonnetdecycle) – Brian Rogers Nov 12 '15 at 00:39

6 Answers6

28

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;
}
Alexander Vasilyev
  • 1,273
  • 2
  • 15
  • 22
  • 1
    - 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); – Alexander Vasilyev May 20 '13 at 05:54
  • 1
    thanks a lot! i spend a lot of time for searching bug! – Ilya Klementiev Mar 21 '14 at 06:15
  • 1
    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]; – pamidur Mar 31 '15 at 14:43
  • Would this work with "nested" arrays? I tried to no luck. – Aron Einhorn Jan 26 '17 at 20:04
  • exactly what i was looking for – FRANCISCO KK Jun 25 '19 at 15:16
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 !

Gaurav
  • 8,367
  • 14
  • 55
  • 90
  • 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. – Rolf Mar 23 '16 at 16:24
13

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>
JLRishe
  • 99,490
  • 19
  • 131
  • 169
  • 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 :-) – Arcord May 11 '17 at 07:58
  • @Arcord Thank you for figuring that out and letting me know! Answer updated. – JLRishe May 11 '17 at 09:16
  • This one is great! – laorient Apr 19 '19 at 16:48
  • 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.. – Boppity Bop Dec 13 '20 at 18:00
  • @BoppityBop I think the answer to that would depend on why the GET is failing. Why is it failing? – JLRishe Dec 14 '20 at 08:20
  • because it throws = circular ref in json.. duh – Boppity Bop Dec 14 '20 at 18:28
  • @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](https://stackoverflow.com/help/how-to-ask). I think you're probably best off asking a new question here. If you link me to it, I can have a look. – JLRishe Dec 15 '20 at 15:55
  • 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.. – Boppity Bop Dec 15 '20 at 16:34
  • @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. – JLRishe Dec 15 '20 at 17:11
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;
    }
evb
  • 61
  • 4
  • 1
    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. ;-( – Alexander Vasilyev Aug 07 '14 at 13:29
  • 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. – Rolf Mar 23 '16 at 16:37
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;
}
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;
}
Otabek Kholikov
  • 1,188
  • 11
  • 19
  • 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. – Eric Mutta Feb 26 '21 at 07:54