Webapi problem when trying to Deserialize object with property of the same type (circular reference)

Asked

Viewed 165 times

1

I’m doing some tests with WebAPI, to see how it behaves with circular reference, and found a curious case.

Model

[DataContract(IsReference = true)]
public class Pessoa
{
    [DataMember(EmitDefaultValue = false, Order = 1)]
    public string Nome { get; set; }

    [DataMember(EmitDefaultValue = false, Order = 2)]
    public Pessoa Patner { get; set; }

    [DataMember(EmitDefaultValue = false, Order = 3)]
    public Pessoa Father { get; set; }

    [DataMember(EmitDefaultValue = false, Order = 4)]
    public Pessoa Mother { get; set; }

    [DataMember(EmitDefaultValue = false, Order = 5)]
    public Pessoa[] Children { get; set; }

    [DataMember(EmitDefaultValue = false, Order = 6)]
    public Pessoa[] Siblings { get; set; }

    public Pessoa(string nome)
    {
        this.Nome = nome;
    }
}

Apicontroller

public async Task<Pessoa[]> ListarFamilia()
{
    var father = new Pessoa("Person 01");
    var mother = new Pessoa("Person 02");
    var child1 = new Pessoa("Person 03");
    var child2 = new Pessoa("Person 04");
    var child3 = new Pessoa("Person 05");

    father.Patner = mother;
    father.Children = new Pessoa[] { child1, child2, child3 };

    mother.Patner = father;
    mother.Children = new Pessoa[] { child1, child2, child3 };

    child1.Father = father;
    child1.Mother = mother;
    child1.Siblings = new Pessoa[] { child2, child3 };

    child2.Father = father;
    child2.Mother = mother;
    child2.Siblings = new Pessoa[] { child1, child3 };

    child3.Father = father;
    child3.Mother = mother;
    child3.Siblings = new Pessoa[] { child1, child2 };

    var familia = new Pessoa[] { father, mother, child1, child2, child3 };
    return familia;
}

public async Task<int> ContarPessoas(Pessoa[] familia)
{
    return familia.Length;
}

Global.asax

GlobalConfiguration.Configure(WebApiConfig.Register);
Newtonsoft.Json.JsonConvert.DefaultSettings = () =>
{
    return new Newtonsoft.Json.JsonSerializerSettings
    {
        ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Serialize,
        PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects
    };
};

When I call the method in /api/Pessoa/ListarFamilia, it returns me the following json

[
    {
        "Nome": "Person 01",
        "Patner": {
            "Nome": "Person 02",
            "Patner": {
                "$ref": 1
            },
            "Children": [
                {
                    "Nome": "Person 03",
                    "Father": {
                        "$ref": 1
                    },
                    "Mother": {
                        "$ref": 2
                    },
                    "Siblings": [
                        {
                            "Nome": "Person 04",
                            "Father": {
                                "$ref": 1
                            },
                            "Mother": {
                                "$ref": 2
                            },
                            "Siblings": [
                                {
                                    "$ref": 3
                                },
                                {
                                    "Nome": "Person 05",
                                    "Father": {
                                        "$ref": 1
                                    },
                                    "Mother": {
                                        "$ref": 2
                                    },
                                    "Siblings": [
                                        {
                                            "$ref": 3
                                        },
                                        {
                                            "$ref": 4
                                        }
                                    ],
                                    "$id": 5
                                }
                            ],
                            "$id": 4
                        },
                        {
                            "$ref": 5
                        }
                    ],
                    "$id": 3
                },
                {
                    "$ref": 4
                },
                {
                    "$ref": 5
                }
            ],
            "$id": 2
        },
        "Children": [
            {
                "$ref": 3
            },
            {
                "$ref": 4
            },
            {
                "$ref": 5
            }
        ],
        "$id": 1
    },
    {
        "$ref": 2
    },
    {
        "$ref": 3
    },
    {
        "$ref": 4
    },
    {
        "$ref": 5
    }
]

so far so good, but when I try to send this same JSON to /api/Pessoa/ContarPessoas, the family object is not completely deserialized as Formatter cannot handle the "$ref".

finally, replaces the ContarPessoas

public async Task<int> ContarPessoas(object request)
{
    var jsonStr = request.ToString();
    var familia = JsonConvert.DeserializeObject<Pessoa[]>(jsonStr);
    return familia.Length;
}

The request arrived correctly, with all the { '$ref': 'n' } in their respective places, but Deserializeobject does not recognize them.

finally, this is the script I’m using on my page html:

<script type="text/javascript">
    JSON.decircle = function (obj) {
        JSON._decircle(obj, []);
    }

    JSON.recircle = function (obj) {
        var refs = [];
        var objs = [];
        JSON._recircleScanIds(obj, refs);
        JSON._recircleScanRefs(obj, objs);

        for (var indice in objs) {
            var obj = objs[indice].obj;
            var key = objs[indice].key;
            var end = obj[key]["$ref"];
            obj[key] = refs[end]
        }
    }

    JSON._decircleCheck = function (obj, key, refs) {
        var child = obj[key];
        if (child && typeof child === "object") {
            if (Array.isArray(child)) {
                for (var indice in child) {
                    JSON._decircleCheck(child, indice, refs)
                }
            } else {
                if (refs.indexOf(child) == -1) {
                    JSON._decircle(child, refs);
                } else {
                    obj[key] = { "$ref": child["$id"] };
                }
            }
        }
    }

    JSON._decircle = function (obj, refs) {
        if (obj && typeof obj === "object") {
            if (Array.isArray(obj)) {
                for (var key in obj) {
                    JSON._decircleCheck(obj, key, refs);
                }
            } else {
                if (refs.indexOf(obj) == -1) {
                    refs.push(obj);
                    obj["$id"] = "" + refs.length;
                    for (var key in obj) {
                        JSON._decircleCheck(obj, key, refs);
                    }
                }
            }
        }
    }

    JSON._recircleScanIds = function (obj, refs) {
        if (obj && typeof obj === "object") {
            if (Array.isArray(obj)) {
                for (var key in obj) {
                    JSON._recircleScanIds(obj[key], refs);
                }
            } else {
                var id = obj["$id"];
                if (id) {
                    refs[id] = obj;
                    delete obj["$id"];
                }

                for (var key in obj) {
                    JSON._recircleScanIds(obj[key], refs);
                }
            }
        }
    }

    JSON._recircleScanRefs = function (obj, objs) {
        if (obj && typeof obj === "object") {
            for (var key in obj) {
                var child = obj[key];
                var isArray = Array.isArray(child);
                if (child && typeof child === "object") {
                    if (!isArray && child["$ref"]) {
                        objs.push({ obj: obj, key: key });
                    } else {
                        JSON._recircleScanRefs(child, objs);
                    }
                }
            }
        }
    }

    var getFamily = function (callback) {
        var httpRequest = new XMLHttpRequest();
        httpRequest.open("POST", "http://localhost:54454/api/Pessoa/ListarFamilia", true);
        httpRequest.responseType = "json";
        httpRequest.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
        httpRequest.addEventListener("readystatechange", function (event) {
            if (httpRequest.readyState == 4) {
                var familia = httpRequest.response;
                JSON.recircle(familia);
                callback(familia);
            }
        })
        httpRequest.send();
    }

    var sendFamily = function (familia, callback) {
        var httpRequest = new XMLHttpRequest();
        httpRequest.open("POST", "http://localhost:54454/api/Pessoa/ContarPessoas", true);
        httpRequest.responseType = "json";
        httpRequest.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
        httpRequest.addEventListener("readystatechange", function (event) {
            if (httpRequest.readyState == 4) {
                var quantidade = httpRequest.response;
                callback(quantidade);
            }
        });

        JSON.decircle(familia);
        httpRequest.send(JSON.stringify(familia));
    }

    getFamily(function (familia) {
        sendFamily(familia, function (quantidade) {
            console.log(quantidade);
        });
    })        
</script>
  • @Ciganomorrisonmendez, in this way I’m ignoring the properties that cause the circular reference and would like to keep the reference to the object. in any case I added an important snippet of my Global.asax, I’m already using ReferenceLoopHandling = ReferenceLoopHandling.Serializeand PreserveReferencesHandling = PreserveReferencesHandling.Objects

1 answer

0

In a Webapi, you don’t need to receive data like object and then deserialize. The namespace System.Web.WebAPI already owns the Newtonsoft.Json how dependency that solves this for you.

Try it like this:

public async Task<int> ContarPessoas([FromBody] Pessoa[] familia)
{
    return familia.Length;
}

Important to know that if the data does not meet the input parameter, the request will be made, and the input value will be null.

Nor is it necessary to set up global.asax. Try without setting anything, sound everything default, should work.

  • Yes, the global.asax was a test, in theory the [Datamember(Isreference = true)] should already be enough for JSON.NET to create the references, but for some reason the Deserializeobject is not working as expected

  • I modified the method to receive Object Response and not Person[] family, to be able to see the string that Controller received, it is correct, with the objects { $ref: n } referencing the respective objects with the property $id = n

Browser other questions tagged

You are not signed in. Login or sign up in order to post.