Print
Category: Software hacking
Hits: 1476

I'm writing this because I spent too much time puzzling over it until I found a few gems. I probably won't use this IRL because it's clunky (and I think I have a way I prefer). but here's ACTUALLY how to write a JSON-serializable class, and why.

The official JSONEncoder docs say: "To use a custom JSONEncoder subclass (e.g. one that overrides the default() method to serialize additional types), specify it with the cls kwarg; otherwise JSONEncoder is used." Less-obvious (at least to me): JSONEncoder can *only* encode native Python types (list, dict, int, string, tuple ... that sort of thing); if you want to encode any other class, you *must* specify cls= in the dump/load methods, and you must provide a JSONEncoder subclass.

Specifically, json.dumps(data, cls=CustomClass) (where CustomClass is a subclass of JSONEncoder, with a default(self, o) method:

class SpecialDataClass(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, self.__class__):
            return { 'data': obj.data } # or whatever serializable native type
        else:
            super().default(obj)

Serialize with: json.dumps(specialData, cls=SpecialDataClass)

This "works," but it broke down (for me) when I wanted to have a class stored within a dict, and I wanted to serialize that dict. Like, json.dumps(dict) fails if dict contains a special class. The class doing the serialization has to know what it's serializing (so it can specify that keyword), but I don't want that level of binding in my code. Generally this will fail if you have >1 special, unrelated classes to deserialize. Yes you can write a custom deserializing class which handles all of them (sort of a Moderator), but c'mon.

What I did instead (and what will probably change): I provided my to-be-serialized class with "serialize" and "deserialize" methods; they return or take simple JSON-friendly data. The enclosing class (PersistentDict) now accepts a "cls=" argument. New values in that dict would be instantiated in this class, and on read/write all values are pre-serialized or post-deserialized for sending down to json's dump/load methods.