lmotl3
lmotl3

Reputation: 647

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

Like the title says, I'm trying to write a custom decoder for an object whose class I've defined which contains other objects whose 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 movies are 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 custom 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 him 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.

Upvotes: 37

Views: 30674

Answers (3)

nmdaz
nmdaz

Reputation: 109

MY ALTERNATIVE CONTRIBUTION: Without "Encoders" or "Decoders"

Use the "_base.Common" class in your object class and inherit methods to manipulate JSON. You do not need to explicitly set the name of the attributes.

Note: All parent and child objects must extend the "_base.Common" class.

Code:

_base.py

import json

class Common:

    def toJson(self):
        return json.dumps(self.toDict())

    def toDict(self): # Fix converter to 'dict' on base of structure of custom class (extended of Common class)
        null = "" # or None
        d = dict()
        for k, v in self.__dict__.items():
            if hasattr(v, "__dict__") and issubclass(self.__getattribute__(k).__class__, Common):
                d[k] = v.toDict() #v.__dict__
            else: 
                d[k] = null if v is None else (v.__str__() if isinstance(v, (int, str, bool)) else (v if isinstance(v, (list, dict)) else str(v)))
        return d

    def fromDict(self, odict: dict):
        self.__dict__ = odict
        return self

    def fromJson(self, ojson: str):
        if ojson is None : return self
        for k, v in json.loads(ojson).items(): 
            if isinstance(v, dict):
                self.__setattr__(k, self.__getattribute__(k).fromDict(v)) # object and sub-objects extents Common class for use method ".fromDict(...)"
            else:    
                self.__setattr__(k, v)            
        return self

    def isEmpty(self):
        for k , v in self.__dict__.items():
            if v is not None:
                if isinstance(v, str) and not __isNullable(v):
                    return False
                elif isinstance(v, list) and len(v) > 0:
                    return False
                elif isinstance(v, dict) and __isNullable(dict(v).__repr__()):
                    return False
        
        return True

    def __isNullable(ostr : str): # Private method
        NULL_VALUES = (None, "", "null", "none", "nan", "[]") # Customizable
        try:
            if ostr is None: return True
            return NULL_VALUES.__contains__(ostr.lower() if ostr is not None else ostr)
        except:
            print("Class object : %s" % ostr.__class__)
            raise "Type object can't evaluate!!"

custom_objects.py

from _base import Common

class ParentObject(Common):

    def __init__(self):
        self.attr_one = None
        self.attr_two = None
        self.child = ChildObject()
        self.colors : lis[ColorObject] = [] 


class ChildObject(Common):

    def __init__(self):
        self.attr_one_ch = None
        self.attr_two_ch = None

class ColorObject(Common):

    def __init__(self):
        self.name = None
        self.desc = None

Now you can do:

main.py

from custom_objects import ParentObject, ChildObject
# String OBJECT JSON
sjson = '{ "attr_one" : "some_one", "attr_two" : "some_two", "child" : { "attr_one_ch" : "child_some_one", "attr_two_ch" : "child_some_two" }, "colors" : [{"name":"red", "desc" : "is red"},{"name":"blue", "desc" : "is blue"}] }'
pobject = ParentObject().fromJson(sjson)
#

print("\n== OUTPUT ==\n")
#
print(pobject.toJson())
#
print(pobject.attr_one)
#
print(pobject.child.attr_one_ch) # Normal
# or
print(pobject.child.__getattribute__("attr_one_ch")) # By name attr
# or
print(pobject.child.__dict__["attr_one_ch"]) # By name in dict of object
#
print(pobject.colors)

OUTPUT:

== OUTPUT ==

{"attr_one": "some_one", "attr_two": "some_two", "child": {"attr_one_ch": "child_some_one", "attr_two_ch": "child_some_two"}, "colors": [{"name": "red", "desc": "is red"}, {"name": "blue", "desc": "is blue"}]}
some_one
child_some_one
child_some_one
child_some_one
[{'name': 'red', 'desc': 'is red'}, {'name': 'blue', 'desc': 'is blue'}]

Upvotes: 0

VirtualScooter
VirtualScooter

Reputation: 1888

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

Upvotes: 23

user3608078
user3608078

Reputation: 346

This problem can be solved without the usage of JSONEncoder or JSONDecoder.

  • add a to_dict() method to each class (takes care of the conversion from python object to JSON dict)
  • if one of your object constructors expects parameter types other than bool, str, int, and float check whether the passed parameters are of type dict, if that's the case you have to construct the object yourself (see constructor of Edge)

Shortened your example a bit:

class Edge:
    def __init__(self, actor, movie):
        if type(actor) is Actor:
            self.actor = actor
        else: # type == dict
            self.actor = Actor(**actor)
        if type(movie) is Movie:
            self.movie = movie
        else: # type == dict
            self.movie = Movie(**movie)

    def __eq__(self, other):
        return (self.movie == other.movie) & (self.actor == other.actor)

    def __str__(self):
        return "".join(["Actor: ", str(self.actor), " /// Movie: ", str(self.movie)])

    def to_dict(self):
        return {"actor": self.actor.to_dict(), "movie": self.movie.to_dict()}

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):
        return self.title == other.title

    def __str__(self):
        return self.title

    def to_dict(self):
        return {"title": self.title, "gross": self.gross, "soup": self.soup, "year": self.year}

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

    def __eq__(self, other):
        return self.name == other.name

    def __str__(self):
        return self.name

    def to_dict(self):
        return {"name": self.name, "age": self.age, "soup": self.soup}


if __name__ == '__main__':
    edge_obj = Edge(Actor("Pierfrancesco Favino", 50, "id0"), Movie("Suburra", 10, "id1", 2015))
    edge_dict = edge_obj.to_dict()
    edge_obj_new = Edge(**edge_dict)

    print("manual edge\t\t", edge_obj)
    print("edge to json\t", edge_dict)
    print("auto edge\t\t", edge_obj_new)
    print("edges equal?\t", edge_obj == edge_obj_new)

Returns:

manual edge      Actor: Pierfrancesco Favino /// Movie: Suburra
edge to json     {'actor': {'name': 'Pierfrancesco Favino', 'age': 50, 'soup': 'id0'}, 'movie': {'title': 'Suburra', 'gross': 10, 'soup': 'id1', 'year': 2015}}
auto edge        Actor: Pierfrancesco Favino /// Movie: Suburra
edges equal?     True

As you can see both Edge objects are equal and the second line outputs the Edge as an dict in JSON notation.

Upvotes: -2

Related Questions