JSON supports bidirectional relationships

Have you ever attempted to construct a JSON data structure containing entities linked by a bidirectional relationship (a circular reference, in other words)? If so, you’ve probably encountered a JavaScript error message similar to “Uncaught TypeError: Converting circular structure to JSON”. Java developers utilizing the Jackson library might have seen “Could not write JSON: Infinite recursion (StackOverflowError) with root cause java.lang.StackOverflowError”.

JSON Bidirectional Relationship Challenge

This piece offers a reliable, practical method for generating JSON structures that encompass bidirectional relationships without triggering these errors.

Proposed solutions to this problem frequently involve workarounds that circumvent the issue without truly resolving it. These workarounds include employing Jackson annotation types like @JsonManagedReference and @JsonBackReference (which simply exclude the back reference from serialization) or utilizing @JsonIgnore to disregard one side of the relationship. Another option is to develop custom serialization code that ignores any bidirectional relationships or circular dependencies within the data.

However, our aim is not to ignore or omit either side of the bidirectional relationship. We want to maintain it, in both directions, without encountering errors. A true solution should accommodate circular dependencies in JSON, freeing developers from constant vigilance and the need for extra steps to rectify them. This article presents a practical and direct technique to achieve this, serving as a valuable tool for modern front-end developers.

Illustrating a Simple Bidirectional Relationship

A common scenario where this bidirectional relationship (also known as a circular dependency) problem occurs is when a parent object references children, and those children, in turn, maintain references back to their parent. Consider this simple example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var obj = {
	"name": "I'm parent"
}

obj.children = [
	{
		"name": "I'm first child",
		"parent": obj
	},
	{
		"name": "I'm second child",
		"parent": obj
	}
]

Attempting to convert the parent object above into JSON (using the stringify method, for instance: var parentJson = JSON.stringify(parent);) will throw the exception Uncaught TypeError: Converting circular structure to JSON.

While we could employ one of the previously mentioned techniques (like using annotations like @JsonIgnore) or simply remove the parent references from the children, these are ways of circumventing rather than solving the problem. Our goal is to achieve a JSON structure that preserves each bidirectional relationship and can be converted to JSON without throwing exceptions.

Towards a Solution

One seemingly obvious step is to assign some form of object ID to each object and then replace the children’s references to the parent object with references to the parent object’s id. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var obj = {
	"id": 100,
	"name": "I'm parent"
}

obj.children = [
	{
		"id": 101,
		"name": "I'm first child",
		"parent": 100
	},
	{
		"id": 102,
		"name": "I'm second child",
		"parent": 100
	}
]

This approach will certainly prevent exceptions arising from bidirectional relationships or circular references. However, a problem remains, becoming apparent when we consider the serialization and deserialization of these references.

The problem is that we would need to know, based on the example above, that every instance of the value “100” refers to the parent object (as that’s its id). This will work fine in the example where the only property with the value “100” is the parent property. But what if we introduce another property with the value “100”? For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
obj.children = [
	{
		"id": 101,
		"name": "I'm first child",
        "priority": 100,  // This is NOT referencing object ID "100"
		"parent": 100     // This IS referencing object ID "100"
	},
	{
		"id": 102,
		"name": "I'm second child",
        "priority": 200,
		"parent": 100
	}
]

Assuming any reference to the value “100” points to an object, our serialization/deserialization code would have no way of knowing that when parent references the value “100”, it IS referencing the parent object’s id, but when priority references the value “100”, it is NOT referencing the parent object’s id. This would lead to the incorrect replacement of the priority value with a reference to the parent object.

You might wonder, “Why not use the property name instead of the property value to identify object ID references?” While this is an option, it’s quite restrictive. It would require us to predefine a set of “reserved” property names always assumed to reference other objects (names like “parent,” “child,” “next,” etc.). Consequently, only these names could be used for object references, and they would always be treated as such. This limitation makes it impractical for most situations.

Therefore, we need to rely on recognizing property values as object references. However, this necessitates ensuring these values are guaranteed to be unique from all other property values. We can achieve this uniqueness by using Globally Unique Identifiers (GUIDs). For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var obj = {
	"id": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc",
	"name": "I'm parent"
}

obj.children = [
	{
		"id": "6616c598-0a0a-8263-7a56-fb0c0e16225a",
		"name": "I'm first child",
        "priority": 100,
		"parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc" // matches unique parent id
	},
	{
		"id": "940e60e4-9497-7c0d-3467-297ff8bb9ef2",
		"name": "I'm second child",
        "priority": 200,
		"parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc" // matches unique parent id
	}
]

Problem solved, right?

Yes, but…

A Fully Automated Solution

Recall our original challenge: we sought to serialize and deserialize objects with bidirectional relationships to/from JSON without encountering exceptions. While the solution above achieves this, it requires manually adding unique ID fields to each object and replacing object references with their corresponding unique IDs. Ideally, we want a solution that automatically handles our existing object references without manual modification.

In an ideal scenario, we could pass a set of objects (containing any combination of properties and object references) through the serializer and deserializer (without encountering exceptions due to bidirectional relationships) and obtain deserialized objects that perfectly match the original input objects.

Our approach is to have the serializer automatically generate and assign a unique ID (using a GUID) to each object. It then replaces any object reference with the corresponding object’s GUID. (Note that the serializer will need to utilize a unique property name for these IDs; our example uses @id, assuming the “@” prefix ensures uniqueness.) The deserializer will then replace any GUID corresponding to an object ID with a reference to that object (also removing the serializer-generated GUIDs from the deserialized objects, thus restoring them to their original state).

Returning to our example, we want to input the following set of objects as is to our serializer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var obj = {
	"name": "I'm parent"
}

obj.children = [
	{
		"name": "I'm first child",
		"parent": obj
	},
	{
		"name": "I'm second child",
		"parent": obj
	}
]

We would then expect the serializer to generate a JSON structure similar to this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
	"@id": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc",
	"name": "I'm parent",
	"children": [
		{
		    "@id": "6616c598-0a0a-8263-7a56-fb0c0e16225a",
			"name": "I'm first child",
			"parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc"
		},
		{
		    "@id": "940e60e4-9497-7c0d-3467-297ff8bb9ef2",
			"name": "I'm second child",
			"parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc"
		},
	]
}

(A JSON formatter can be used to enhance the readability of any JSON object.)

Feeding this JSON to the deserializer would then produce the original set of objects (the parent object and its two children, correctly referencing each other).

Now that we understand the desired outcome and approach, let’s implement it.

JavaScript Serializer Implementation

Below is a functional JavaScript implementation of a serializer that handles bidirectional relationships without throwing exceptions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
var convertToJson = function(obj) {

    // Generate a random value structured as a GUID
    var guid = function() {
        function s4() {
            return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
        }

        return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
    };

    // Check if a value is an object
    var isObject = function(value) {
        return (typeof value === 'object');
    }
    
    // Check if an object is an array
    var isArray = function(obj) {
        return (Object.prototype.toString.call(obj) === '[object Array]');
    }
    
    var convertToJsonHelper = function(obj, key, objects) {
        // Initialize objects array and 
        // put root object into if it exist
        if(!objects) {
            objects = [];
    
            if (isObject(obj) && (! isArray(obj))) {
                obj[key] = guid();
                objects.push(obj);
            }
        }
    
        for (var i in obj) {
            // Skip methods
            if (!obj.hasOwnProperty(i)) {
                continue;
            }
    
            if (isObject(obj[i])) {
                var objIndex = objects.indexOf(obj[i]);
    
                if(objIndex === -1) {
                    // Object has not been processed; generate key and continue
                    // (but don't generate key for arrays!)
                    if(! isArray(obj)) {
                        obj[i][key] = guid();
                        objects.push(obj[i]);
                    }
 
                    // Process child properties
                    // (note well: recursive call)
                    convertToJsonHelper(obj[i], key, objects);
                } else {
                    // Current object has already been processed;
                    // replace it with existing reference
                    obj[i] = objects[objIndex][key];
                }
            }
        }
    
        return obj;
    }

    // As discussed above, the serializer needs to use some unique property name for
    // the IDs it generates. Here we use "@id" since presumably prepending the "@" to
    // the property name is adequate to ensure that it is unique. But any unique
    // property name can be used, as long as the same one is used by the serializer
    // and deserializer.
    //
    // Also note that we leave off the 3rd parameter in our call to
    // convertToJsonHelper since it will be initialized within that function if it
    // is not provided.
    return convertToJsonHelper(obj, "@id");
}

JavaScript Deserializer Implementation

Below is a functional JavaScript implementation of a deserializer that handles bidirectional relationships without throwing exceptions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
var convertToObject = function(json) {

    // Check if an object is an array
    var isObject = function(value) {
        return (typeof value === 'object');
    }
    
    // Iterate object properties and store all reference keys and references
    var getKeys = function(obj, key) {
        var keys = [];
        for (var i in obj) {
            // Skip methods
            if (!obj.hasOwnProperty(i)) {
                continue;
            }

            if (isObject(obj[i])) {
                keys = keys.concat(getKeys(obj[i], key));
            } else if (i === key) {
                keys.push( { key: obj[key], obj: obj } );
            }
        }

        return keys;
    };
    
    var convertToObjectHelper = function(json, key, keys) {
        // Store all reference keys and references to object map
        if(!keys) {
            keys = getKeys(json, key);
    
            var convertedKeys = {};
    
            for(var i = 0; i < keys.length; i++) {
                convertedKeys[keys[i].key] = keys[i].obj;
            }
    
            keys = convertedKeys;
        }
    
        var obj = json;

        // Iterate all object properties and object children 
        // recursively and replace references with real objects
        for (var j in obj) {
            // Skip methods
            if (!obj.hasOwnProperty(j)) {
                continue;
            }
    
            if (isObject(obj[j])) {
                // Property is an object, so process its children
                // (note well: recursive call)
                convertToObjectHelper(obj[j], key, keys);
            } else if( j === key) {
                // Remove reference id
                delete obj[j];
            } else if (keys[obj[j]]) {
                // Replace reference with real object
                obj[j] = keys[obj[j]];
            }
        }
    
        return obj;
    };

    // As discussed above, the serializer needs to use some unique property name for
    // the IDs it generates. Here we use "@id" since presumably prepending the "@" to
    // the property name is adequate to ensure that it is unique. But any unique
    // property name can be used, as long as the same one is used by the serializer
    // and deserializer.
    //
    // Also note that we leave off the 3rd parameter in our call to
    // convertToObjectHelper since it will be initialized within that function if it
    // is not provided.
    return convertToObjectHelper(json, "@id");
}

Passing a set of objects (including those with bidirectional relationships) through these two methods effectively functions as an identity function; that is, convertToObject(convertToJson(obj)) === obj evaluates to true.

Java/Jackson Example

Let’s explore how this approach is supported in popular external libraries, using Java with the Jackson library as an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class, property="@id")
public class Parent implements Serializable {
   private String name;

   private List<Child> children = new ArrayList<>();

   public String getName() {
   		return name;
   }

   public void setName(String name) {
   		this.name = name;
   }

   public List<Child> getChildren() {
   		return children;
   }

   public void setChildren(List<Child> children) {
   		this.children = children;
   }
}

@JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class, property="@id")
public class Child implements Serializable {
   private String name;

   private Parent parent;

   public String getName() {
   		return name;
   }

   public void setName(String name) {
   		this.name = name;
   }

   public Parent getParent() {
   		return parent;
   }

   public void setParent(Parent parent) {
   		this.parent = parent;
   }
}

These two Java classes, Parent and Child, mirror the structure of the JavaScript example presented earlier. The key takeaway here is the use of the @JsonIdentityInfo annotation, which instructs Jackson on how to serialize/deserialize these objects.

Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Parent parent = new Parent();
parent.setName("I'm parent")

Child child1 = new Child();
child1.setName("I'm first child");

Child child2 = new Child();
child2.setName("I'm second child");

parent.setChildren(Arrays.asList(child1, child2));

Serializing the parent instance to JSON will result in the same JSON structure as the JavaScript example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
	"@id": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc",
	"name": "I'm parent",
	"children": [
		{
		    "@id": "6616c598-0a0a-8263-7a56-fb0c0e16225a",
			"name": "I'm first child",
			"parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc"
		},
		{
		    "@id": "940e60e4-9497-7c0d-3467-297ff8bb9ef2",
			"name": "I'm second child",
			"parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc"
		},
	]
}

An Additional Benefit

This approach to handling bidirectional relationships in JSON also helps reduce JSON file size. By referencing objects via their unique IDs, we avoid including redundant copies of the same object.

Consider this example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
	"@id": "44f47be7-af77-9a5a-8606-a1e6df299ec9",
	"id": 1,
	"name": "I'm parent",
	"children": [
		{
			"@id": "54f47be7-af77-9a5a-8606-a1e6df299eu8",
			"id": 10,
			"name": "I'm first child",
			"parent": "44f47be7-af77-9a5a-8606-a1e6df299ec9"
		},
		{
			"@id": "98c47be7-af77-9a5a-8606-a1e6df299c7a",
			"id": 11,
			"name": "I'm second child",
			"parent": "44f47be7-af77-9a5a-8606-a1e6df299ec9"
		},
		{
			"@id": "5jo47be7-af77-9a5a-8606-a1e6df2994g2",
			"id": 11,
			"name": "I'm third child",
			"parent": "44f47be7-af77-9a5a-8606-a1e6df299ec9"
		}
	],
	"filteredChildren": [
		"54f47be7-af77-9a5a-8606-a1e6df299eu8", "5jo47be7-af77-9a5a-8606-a1e6df2994g2"
	]
}

The filteredChildren array demonstrates that we can simply include object references in our JSON instead of replicating the referenced objects and their content.

Conclusion

This solution enables you to eliminate circular reference exceptions during JSON serialization while minimizing constraints on your objects and data. If your JSON serialization libraries lack a similar solution, you can implement your own based on the provided example. I hope you find this helpful.

Licensed under CC BY-NC-SA 4.0