How to write a custom JSON decoder for a complex object?

lmotl3 picture lmotl3 · Feb 26, 2018 · Viewed 10.3k times · Source

Like the title says, I'm trying to write a custom decoder for an object whose class I've defined which contains other objects who class I've defined. The "outer" class is an Edge, defined like so:

class Edge:
    def __init__(self, actor, movie):
        self.actor = actor
        self.movie = movie

    def __eq__(self, other):
        if (self.movie == other.movie) & (self.actor == other.actor):
            return True
        else:
            return False

    def __str__(self):
        print("Actor: ", self.actor, " Movie: ", self.movie)

    def get_actor(self):
        return self.actor

    def get_movie(self):
        return self.movie

with the "inner" classes actor and movie defined like so:

class Movie:
    def __init__(self, title, gross, soup, year):
        self.title = title
        self.gross = gross
        self.soup = soup
        self.year = year

    def __eq__(self, other):
        if self.title == other.title:
            return True
        else:
            return False

    def __repr__(self):
        return self.title

    def __str__(self):
        return self.title

    def get_gross(self):
        return self.gross

    def get_soup(self):
        return self.soup

    def get_title(self):
        return self.title

    def get_year(self):
        return self.year

class Actor:
    def __init__(self, name, age, soup):
        self.name = name
        self.age = age
        self.soup = soup

    def __eq__(self, other):
        if self.name == other.name:
            return True
        else:
            return False

    def __repr__(self):
        return self.name

    def __str__(self):
        return self.name

    def get_age(self):
        return self.age

    def get_name(self):
        return self.name

    def get_soup(self):
        return self.soup

(soup is just a beautifulsoup object for that movie/actor's Wikipedia page, it can be ignored). I've written a customer encoder for the edge class as well:

class EdgeEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, Edge):
            return {
                    "Actor": {
                             "Name": o.get_actor().get_name(),
                             "Age": o.get_actor().get_age()
                             },
                    "Movie": {
                             "Title": o.get_movie().get_title(),
                             "Gross": o.get_movie().get_gross(),
                             "Year": o.get_movie().get_year()
                             }
                    }
        return json.JSONEncoder.default(self, o)

which I've tested, and it properly serializes a list of edges into a JSON file. Now my problem comes when trying to write an edge decoder. I've used the github page here as a reference, but my encoder deviates from his and I'm wondering if it's necessary to change it. Do I need to explicitly encode an object's type as its own key-value pair within its JSON serialization the way he does, or is there some way to grab the "Actor" and "Movie" keys with the serialization of the edge? Similarly, is there a way to grab "Name". "Age", etc, so that I can reconstruct the Actor/Movie object, and then use those to reconstruct the edge? Is there a better way to go about encoding my objects instead? I've also tried following this tutorial, but I found the use of object dicts confusing for their encoder, and I wasn't sure how to extend that method to a custom object which contains custom objects.

Answer

VirtualScooter picture VirtualScooter · Sep 3, 2018

The encoder/decoder example you reference (here) could be easily extended to allow different types of objects in the JSON input/output.

However, if you just want a simple decoder to match your encoder (only having Edge objects encoded in your JSON file), use this decoder:

class EdgeDecoder(json.JSONDecoder):
    def __init__(self, *args, **kwargs):
        json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)
    def object_hook(self, dct):
        if 'Actor' in dct:
            actor = Actor(dct['Actor']['Name'], dct['Actor']['Age'], '')
            movie = Movie(dct['Movie']['Title'], dct['Movie']['Gross'], '', dct['Movie']['Year'])
            return Edge(actor, movie)
        return dct

Using the code from the question to define classes Movie, Actor, Edge, and EdgeEncoder, the following code will output a test file, then read it back in:

filename='test.json'
movie = Movie('Python', 'many dollars', '', '2000')
actor = Actor('Casper Van Dien', 49, '')
edge = Edge(actor, movie)
with open(filename, 'w') as jsonfile:
    json.dump(edge, jsonfile, cls=EdgeEncoder)
with open(filename, 'r') as jsonfile:
    edge1 = json.load(jsonfile, cls=EdgeDecoder)
assert edge1 == edge