During my first encounter of handling JSON (de)serialization in Python, I faced the problem of (de)serializing objects that have properties that are instances of another class.
Using the json module, one has to write two methods, a complex_handler and a class_mapper that are fed to json.dumps and json.loads respectively.
The design problem here is that the class_mapper needs to compare a dict that is to be deserialized with the properties of potential classes in order to find the matching type. In the first level of deserialization one could potentially provide the type as a parameter to the deserialization-function, but as soon as there is a child property, the type may be unknown.
Therefore each class that is to be deserialized has to be registered into a collection of (properties) -> class mappings.
To simplify the handling, I wrote the following JsonConvert class:
import json
class JsonConvert(object):
mappings = {}
@classmethod
def class_mapper(clsself, d):
for keys, cls in clsself.mappings.items():
if keys.issuperset(d.keys()): # are all required arguments present?
return cls(**d)
else:
# Raise exception instead of silently returning None
raise ValueError('Unable to find a matching class for object: {!s}'.format(d))
@classmethod
def complex_handler(clsself, Obj):
if hasattr(Obj, '__dict__'):
return Obj.__dict__
else:
raise TypeError('Object of type %s with value of %s is not JSON serializable' % (type(Obj), repr(Obj)))
@classmethod
def register(clsself, cls):
clsself.mappings[frozenset(tuple([attr for attr,val in cls().__dict__.items()]))] = cls
return cls
@classmethod
def ToJSON(clsself, obj):
return json.dumps(obj.__dict__, default=clsself.complex_handler, indent=4)
@classmethod
def FromJSON(clsself, json_str):
return json.loads(json_str, object_hook=clsself.class_mapper)
@classmethod
def ToFile(clsself, obj, path):
with open(path, 'w') as jfile:
jfile.writelines([clsself.ToJSON(obj)])
return path
@classmethod
def FromFile(clsself, filepath):
result = None
with open(filepath, 'r') as jfile:
result = clsself.FromJSON(jfile.read())
return result
Now we can define some classes and make them (de)serializable by decorating them with JsonConvert.register:
@JsonConvert.register
class Employee(object):
def __init__(self, Name:int=None, Age:int=None):
self.Name = Name
self.Age = Age
return
@JsonConvert.register
class Company(object):
def __init__(self, Name:str="", Employees:[Employee]=None):
self.Name = Name
self.Employees = [] if Employees is None else Employees
return
When the class definition is parsed, this will register it with the static mappings in JsonConvert.
Now we can easily serialize and deserialize our Company to a JSON-string:
company = Company("Contonso")
company.Employees.append(Employee("Werner", 38))
company.Employees.append(Employee("Mary"))
asJson = JsonConvert.ToJSON(company)
fromJson = JsonConvert.FromJSON(asJson)
asJsonFromJson = JsonConvert.ToJSON(fromJson)
assert(asJsonFromJson == asJson)
print(asJsonFromJson)
Or directly to and from a file:
filepath = JsonConvert.ToFile(company, "company.json") fromFile = JsonConvert.FromFile(filepath)
The JSON string looks like this:
{
"Name": "Contonso",
"Employees": [
{
"Name": "Werner",
"Age": 38
},
{
"Name": "Mary",
"Age": null
}
]
}
It’s not as convenient as our beloved Json.NET for C#/.NET, but it can get the job done.
cheers
PS.: if you want to learn more about decorators, have a look at this excellent YouTube tutorial.
1oglop1
Hi, nice work!
I noticed that you adopted the convention from Json.NET but this is python and this code violates PEP8 and probably other PEPs too, could you please fix it? with black and pylint/flake8?
In case you need any help or you are curious about it, let me know 😉
theCake Post author
Thanks!
Unfortunately, I have to admit you are absolutely right…
I’ll put it on my list, but due to a busy week, you’re of course invited to propose a revision.
cheers
Anonymous
I get this error with Python 3.6:
File “C:\Projects\PycharmProjects\Random\lib\JsonConvert.py”, line 31, in ToJSON
return json.dumps(obj.__dict__, default=clsself.complex_handler, indent=4)
AttributeError: ‘list’ object has no attribute ‘__dict__’
Any ideas?
Nitin Muteja`
What if two classes have same attribute names? For instance a response object from two service? How do we handle the conflict in that case?
theCake Post author
Yes, that’s problematic with the code I showed. Note that my post is from 2016..
I guess you could include the class name with in the key that’s used in the `mappings` dict.