JSON - Custom types
JSON works well when you use simple, supported, built-in types.
What if you want to serialise something more complicated, like a custom class?
JSON encoder of custom types
import json
class Vector:
def __init__(self, a, b):
self.a = a
self.b = b
def __add__(self, other):
return Vector(self.a + other.a, self.b + other.b)
def __str__(self):
return f"Vector ({self.a}, {self.b})"
v = Vector(3, 4)
json.dumps(v) ## TypeError: Object of type Vector is not JSON serializable
We receive an error complaint from Python, that Object of type Vector is not JSON serialisable.
So, how do we solve this problem?
- The first method would be the “Keep It Simple” method: Just represent your data with simple built-in types that we know JSON supports! I personally keep it that way. I would choose a different serialisation format if I need to serialise complicated objects.
- The second method is to exploit your newly acquired OOP prowess, and subclass the standard
JSONEncoder
and override itsdefault()
method. Ooh… behold the mighty power of OOP! 😆
class VectorEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Vector):
return (obj.a, obj.b)
else:
return super().default(obj)
So in the code above, you override the default()
method, and check if the object is a Vector
. If it is, you return a tuple (a, b)
which is supported by JSON. Otherwise, you just pass it back to the default()
method of your superclass.
You can then just use the json.dump()
method with the cls
parameter to your new VectorEncoder
:
json_string = json.dumps(v, cls=VectorEncoder)
print(json_string) ## [3, 4]
Alternatively, you can invoke the encode()
method of VectorEncoder
.
encoder = VectorEncoder()
json_string = encoder.encode(v)
print(json_string)
JSON decoder of custom types
We have solved the issue of encoding, but what about deserialisation or decoding?
In our earlier example, our JSON string is "[3, 4]"
. If we try to decode this, we will end up with a list [3, 4], rather than a Vector
. There is just not enough information to reconstruct Vector
.
So we first need to modify our VectorEncoder
from earlier so that it stores some information that this is a Vector
. So this time, we use a dictionary rather than a tuple.
class VectorEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Vector):
return {"__vector__": True, "a": obj.a, "b": obj.b}
else:
return super().default(obj)
v = Vector(3, 4)
json_string = json.dumps(v, cls=VectorEncoder)
print(json_string) ## {"__vector__": true, "a": 3, "b": 4}
So now, we have stored Vector
as a dictionary, with a key "__vector__"
that will later tell us that this is meant to be a Vector
.
Let us deserialise this json_string
back into our Vector
class then!
Like Encoding, we will use a custom decoder decode_vector
to decode the json string back into a Vector
. We use the object_hook
parameter of json.loads()
for it to use our custom decoder.
def decode_vector(dct):
if "__vector__" in dct:
return Vector(dct["a"], dct["b"])
else:
return dct
data = json.loads(json_string, object_hook=decode_vector)
print(data) ## Vector (3, 4)
And we have managed to reproduce our Vector
from JSON!