Introduction
There are some common challenges while transferring data between server and client in JSON format. I’m trying here to address few of them:
- Serialize/Deserialize object by reference or preserve association between objects
- Serialize/Deserialize circular references
Serializers like DataContractJsonSerializer and JavaScriptSerializer are serializing objects by value. Result is loss of object references or association between objects. That means,if an object is referenced by other objects, that object will be copied locally to all those objects. So, it's a data duplicacy. Another challenge is these serializers do not support circular references and throw exception.
Ideally, if instance of an object is referenced at multiple places in object graph, only single instance should be created during deserialization. And that created instance should be referenced from everywhere else. Important thing is that solution should work for server and client both. So that data transfer from server to client and client to server should retain associations between objects after deserialize.
Use case
Let’s begin with following sample class diagram. Here Branch and Customer classes are associated with multiple classes. Also there is a circular reference in Account class.
<img alt="Sample Class Diagram" src="http://www.codeproject.com/KB/aspnet/1121469/class_diagram.png" style="width: 627px; height: 288px;" />
Generate classes based on above diagram. Try to serialize branch object into JSON format:
IEnumerable
branches = Branch.GetAllBranches();
var js = new JavaScriptSerializer();
string json = js.Serialize(branches);
</branch>
It will throw following exception:
<img alt="Circular Reference Exception" src="http://www.codeproject.com/KB/aspnet/1121469/circular_reference_error.png" style="width: 452px; height: 68px;" />
Even if there are no circular references, it will serialize object by value. Resulting loss of object references and data duplicity. In this case, duplicate Customer and Branch data will be generated inside Account and Loan.
Server side solution (C#, Web API):
So, let’s move to fix these issues with Json.NET. Json.NET offers many features not found in the JavaScriptSerializer and DataContractSerializer. Like serializing objects by reference, circular references, LINQ to JSON, ISO8601 dates, non-default constructors, interface based deserialization and many more
First, you need to install Json.NET, use following NuGet command to install Json.NET:
Install-Package Newtonsoft.Json
I got Newtonsoft.Json 9.0.1 but you may have other version. Next, add Json.NET namespace in your code:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
All setup done, it’s time to implement the solution. Make changes in our previous solution and replace JavaScriptSerializer with this new one.
public HttpResponseMessage GetAllBranches()
{
var branches = Branch.GetAllBranches();
string json = JsonConvert.SerializeObject(branches, Formatting.Indented, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects });
return Request.CreateResponse(HttpStatusCode.OK, json);
}
Here Json.NET serializes object by reference rather than by value. It assigns each object a "$id" value e.g., {"$id":"1"}, and then later just refers to that object using that id e.g., {"$ref":"1"}. And so far as I can tell, when it is Json.NET (using C#/.NET) that is deserializing those references, it places the appropriate instances of the object in the appropriate places. It solves both issues; preserve association between objects and circular reference. Use below code block to deserialize json data transferred from client code:
public HttpResponseMessage PostBranches(JObject jsonData)
{
dynamic m = jsonData;
string jBranches = m.branches.Value as string;
List
deserializedBranches = JsonConvert.DeserializeObject<list
>(jBranches, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects });
return Request.CreateResponse(HttpStatusCode.OK);
}
</list
</branch>
Client side solution (javascript):
All good so far - but what's the best way to deserialize these in JavaScript, so that I actually get the appropriate object instances in the appropriate places, instead of just weird $ref fields? And also, incorporate $ref in json for each references in javascript objects, so that I can retain object references at server side.
I found a javascript library JsonNetDecycle.js from github. Made few changes into that. Idea is to replace "$ref" with reference object while deserializing. During serialize, add "$id" for each unique object and replace object reference with "$ref" and corresponding object id.
Code to deserializing JSON data is:
var jsObject = retrocycle(JSON.parse(jsonData));
function retrocycle(obj) {
var catalog = [];
findReferences(obj, catalog);
return resolveReferences(obj, catalog);
}
function findReferences(obj, catalog) {
var i;
if (obj && typeof obj === "object") {
var id = obj.$id;
if (typeof id === "string") {
catalog[id] = obj;
}
if (Object.prototype.toString.apply(obj) === "[object Array]") {
for (i = 0; i < obj.length; i += 1) {
findReferences(obj[i], catalog);
}
}
else {
for (name in obj) {
if (obj.hasOwnProperty(name)) {
if (typeof obj[name] === "object") {
findReferences(obj[name], catalog);
}
}
}
}
}
}
function resolveReferences(obj, catalog) {
var i, item, name, id;
if (obj && typeof obj === "object") {
if (Object.prototype.toString.apply(obj) === "[object Array]") {
for (i = 0; i < obj.length; i += 1) {
item = obj[i];
if (item && typeof item === "object") {
id = item.$ref;
if (typeof id === "string") {
obj[i] = catalog[id];
}
else {
obj[i] = resolveReferences(item, catalog);
}
}
}
}
else if (obj.$values && Object.prototype.toString.apply(obj.$values) === "[object Array]") {
var arr = new Array();
for (i = 0; i < obj.$values.length; i += 1) {
item = obj.$values[i];
if (item && typeof item === "object") {
id = item.$ref;
if (typeof id === "string") {
arr[i] = catalog[id];
}
else {
arr[i] = resolveReferences(item, catalog);
}
}
else {
arr[i] = item;
}
}
obj = arr;
}
else {
for (name in obj) {
if (obj.hasOwnProperty(name)) {
if (typeof obj[name] === "object") {
item = obj[name];
if (item) {
id = item.$ref;
if (typeof id === "string") {
obj[name] = catalog[id];
}
else {
obj[name] = resolveReferences(item, catalog);
}
}
}
}
}
}
}
return obj;
}
function removeAutoGenProperty(catalog) {
for (i = 0; i < catalog.length; i += 1) {
var obj = catalog[i];
if (obj && typeof obj === "object") {
var id = obj['$id'];
if (typeof id != "undefined") {
delete obj['$id'];
}
}
}
}
Call following function to Serialize javascript object to JSON, which will be further used by Web API to Deserialize. Function decycle takes object as input parameter and returns JSON.
function decycle(obj) {
var catalog = []; var newObj = getDecycledCopy(obj, catalog);
return newObj;
}
function getDecycledCopy(obj, catalog) {
var i; var name; var nu; switch (typeof obj) {
case "object":
if (obj === null || obj instanceof Boolean || obj instanceof Date || obj instanceof Number || obj instanceof RegExp || obj instanceof String) {
return obj;
}
for (i = 0; i < catalog.length; i += 1) {
if (catalog[i] === obj) {
return { $ref: i.toString() };
}
}
obj.$id = catalog.length.toString();
catalog.push(obj);
if (Object.prototype.toString.apply(obj) === "[object Array]") {
nu = [];
for (i = 0; i < obj.length; i += 1) {
nu[i] = getDecycledCopy(obj[i], catalog);
}
}
else {
nu = {};
for (name in obj) {
if (Object.prototype.hasOwnProperty.call(obj, name)) {
nu[name] = getDecycledCopy(obj[name], catalog);
}
}
}
return nu;
case "number":
case "string":
case "boolean":
default:
return obj;
}
}
Output JSON data after serialization will looks like:
{
"$id": "1",
"AccountNumber": "IN-1700774952",
"Balance": 0.0,
"OpeningDate": "2016-08-23T14:39:55.8165458+05:30",
"Branch": {
"$id": "2",
"Code": "IN-Delhi-1700774952",
"HeadOffice": {
"$ref": "2"
},
"Bank": {
"$id": "3",
"Name": "State Bank Of India",
"Code": "SBI"
},
"City": "Delhi"
},
"Customer": null
}, …
Final Thoughts:
So here you have it, a way to serialize and deserialize objects by reference through Json.NET and make it compatible with client using javascript library JsonNetDecycle.js at client end. It keeps references intact. Also will prevent redundant data and circular reference issue.
I'll be more than happy for your feedback.