JSON Serialization in Python
In C#, it is really easy to convert between class and JSON object, which brings tremendous convenience for Web development. Recently, I have to use a Python framework Django, and found that there’s no such feature as C#, so I implemented a shabby replica for that by myself.
What you will learn
In this article, you will get both a serializer and a de-serializer in Python to convert JSON string or
dict
object into or from a Python object. 😋
Prerequisites
To start with, you must keep one concept in mind,
"Python object is merely a dictionary!"
Not that absolute, but generally speaking, yes.
We do not begin with scratch, and Python already make json
package part of its standard. So we just use it for fundamental parsing.
So… Let’s start serialization! 😆
1. JSON Encoder & Decoder
1.1 JSON Encoder
For basic conversion between JSON string and Python dict
, we can simply use json
module to do the stuffs. However, if you have datetime
field in your Python object, you have to manually convert it to string, as JSON doesn’t have corresponding datetime value.
When you use json.dump
, you need a custom encoder to handle datetime
field.
1 | class AdvancedEncoder(json.JSONEncoder): |
Notice that,
datetime
is derived fromdate
, so you should place instance check ofdatetime
ahead ofdate
. And the format is custom, just make sure they corresponds each other in Encoder and Decoder. 🫡
If you are time zone aware, for example in Django project with USE_TZ
true, you may need to add an extra conversion. Just add a astimezone
method before strftime
for datetime.datetime
class.
1 | obj.astimezone(datetime.timezone(datetime.timedelta(hours=8))).strftime("%Y-%m-%d %H:%M:%S") |
Emm…
datetime.date
doesn’t seem to have such a method. Perhaps because it does not support timezone.
1.2 JSON Decoder
Then, correspondingly, when you use json.loads
, you need a custom decoder. However, what is different is that we need to override its object_hook
to tell it to try convert str
into datetime
object. (It seems, in previous version, it is a method, instead of a member.)
1 | def object_hook(obj): |
After you spot a str
member, you should first check the format of it. Because strptime(value, '%Y-%m-%d %H:%M:%S')
will also treat 2023-06-20
as a valid one and return 2023-06-20 00:00:00
, which will leave no chance for datetime.date
.
And if you find out that the str
should be a datetime.date
, you still have to parse it with datetime.datetime
, because the other doesn’t support this method. Then, you need to convert it do datetime.date
manually.
2. Serialization
Then, with JSONEncoder
, you can simply serialize a object into JSON string.
1 | def serialize(obj) -> str: |
Serialization related exceptions will be talked about at the end, since they are not the main topic.
If you don’t want to serialize a object into raw string, you can also serialize it into a dict
. Here I use deserialize
that will be elaborated later.
1 | def serialize_as_dict(obj): |
3. Deserialization
3.1 Deserialization
Compared to serialization, deserialization got one more problem - how to deserialize a JSON object into a object with desired class type? And this is the key point of this article.
Then, the deserialization can be implemented as such. If cls
is assigned, it will try to convert JSON string or object into the given class, and raise exception if type mismatch. Otherwise, it will simply return a dict
object.
1 | def deserialize(obj, cls=None): |
There are two functions that play an important role here, _check_type
and _construct_cls
. They make sure the JSON object strictly match the given class, and try to build such a class from the JSON object. The implementation of them may be a little hard to understand, though. 😣
3.2 Check Type
Well, although we can just construct class directly, and raise exception if anything wrong happens, we check the type first to give more detailed information if type mismatch occurs. The principle of this process is to convert target class into dict
first, and then compare all fields recursively. You just need to pay more attention to dict
and list
.
1 | def _check_type(src, cls): |
And here is how exactly dict
and list
are handled here.
1 | def __check_type_dict(src, model): |
Now, you can check if a dict
is exactly a desired class or not.
3.3 Construct Class
After you check the type, you can go build a class from it. This is a little tricky, and was buggy. I refined it many times, and I guess it now should work in most cases. At least, no bug encountered ever after.
1 | def _construct_cls(src, cls): |
So… again, Python object is just a dict
with some extra fields. Emm… Hope it could make it easier for you to understand the code. 🥺
4. Exception Declaration
At last, I present to you the definition of custom exceptions that I used, to fit the last piece.
1 | class JsonException(Exception): |
So, this is it. And… I guess that Python is not that diabolical. It can be convenient sometimes. Only, some times. 😶🌫️