What would be the pythonic way to go to prevent circular loop while writing JSON?
General JSONEncoder
class that prevents circular reference error
The following encoder class MyEncoder
performs recursive encoding of the nested objects until a circular reference is detected, whose "name" attribute is returned instead of the object itself.
import json
class MyEncoder(json.JSONEncoder):
def __init__(self, *args, **argv):
super().__init__(*args, **argv)
self.proc_objs = []
def default(self, obj):
if isinstance(obj,(A,B)):
if obj in self.proc_objs:
return obj.name # short circle the object dumping
self.proc_objs.append(obj)
return obj.__dict__
return obj
json.dumps(list_of_As, cls=MyEncoder, check_circular=False, indent=2)
Output:
[
{ "name": "firstA",
"my_Bs": [
{ "name": "secondB",
"my_As": [ "firstA" ]
}
]
},
{ "name": "secondA", "my_Bs": [] }
]
Using a custom toJSON
method
You can implement a serializer method in your classes.
class JSONable:
def toJSON(self):
d = dict()
for k,v in self.__dict__.items():
# save a list of "name"s of the objects in "my_As" or "my_Bs"
d[k] = [o.name for o in v] if isinstance(v, list) else v
return d
class A(JSONable):
def __init__(self,name):
self.name = name
self.my_Bs = []
def register(self,b):
self.my_Bs.append(b)
class B(JSONable):
def __init__(self,name):
self.name = name
self.my_As = []
def register(self,a):
self.my_As.append(a)
json.dumps(list_of_As, default=lambda x: x.toJSON(), indent=2)
Output:
[
{ "name": "firstA", "my_Bs": [ "secondB" ] },
{ "name": "secondA", "my_Bs": [] }
]
The best-practice approach is to record the id()
values of objects already seen, when encoding. id()
values are unique for objects with overlapping lifetimes, and when encoding, you can generally count on the objects not being short-lived. This works on any object type, and doesn't require the objects to be hashable.
Both the copy
and pickle
modules use this technique in a memo
dictionary that maps id()
values to their object for later reference.
You can use this technique here too; you really only need to keep a set of the ids to detect that you can return the .name
attribute. Using a set makes testing for repeated references fast and efficient (membership testing takes O(1) constant time, as opposed to lists, which take O(N) linear time):
class CircularEncoder(json.JSONEncoder):
def __init__(self, *args, **kwargs):
kwargs['check_circular'] = False # no need to check anymore
super(CircularEncoder, self).__init__(*args, **kwargs)
self._memo = set()
def default(self, obj):
if isinstance(obj, (A, B)):
d = id(obj)
if d in self._memo:
return obj.name
self._memo.add(d)
return vars(obj)
return super(CircularEncoder, self).default(obj)
then use json.dumps()
with this class:
json.dumps(list_of_As, cls=CircularEncoder)
For your sample input, this produces:
>>> print(json.dumps(list_of_As, cls=CircularEncoder, indent=2))
[
{
"name": "firstA",
"my_Bs": [
{
"name": "secondB",
"my_As": [
"firstA"
]
}
]
},
{
"name": "secondA",
"my_Bs": []
}
]