0
\$\begingroup\$

I am trying to convert objects inside an object(arbitrary objects) to Json and later retrieve it. I have developed some codes and would like to share if there is any problem in the code.

import json

Here is the first class

class Foo():
    def __init__(self,x,y,bar):
        self.x =x
        self.y = y
        self.bar = bar #Second class object is here

    def toJson(self):
        return json.dumps(Foo(self.x,self.y,self.bar).__dict__)

    @staticmethod
    def fromJson(jsonData):
        data =  json.loads(jsonData)
        return Foo(
            x = data['x'],
            y = data['y'],
            bar = Bar.fromJson(data['bar'])
        )

Here is the second class

class Bar():
    def __init__(self, z):
        self.z = z


    def toJson(self):
        return json.dumps(Bar(self.z).__dict__)

    @staticmethod
    def fromJson(jsonData):
        data =  json.loads(jsonData)
        return Bar(
            z = data['z'],
        )

Convert to Json

jsonData = Foo(100,500,Bar(900).toJson()).toJson()

Retrieve the Json and convert to Object

foo = Foo.fromJson(jsonData)

Print the Object attributes

print(foo.bar.z)

It works actually. But is there any memory leakage? Any security issue?

\$\endgroup\$
  • 1
    \$\begingroup\$ What's the matter with your class naming? \$\endgroup\$ – Mast Jul 6 at 6:52
  • 3
    \$\begingroup\$ Is this how your actual code looks? Please take a look at the help center and note we need to see the real deal. Obfuscated code is not reviewable. \$\endgroup\$ – Mast Jul 6 at 6:53
  • \$\begingroup\$ Yes, why not. This is the concept foo is an object which has another object bar. My question is only that is this right way to serialize and deserialize Python objects. \$\endgroup\$ – Yunus Jul 6 at 6:58
1
\$\begingroup\$

This is not a good interface to serialize to JSON:

jsonData = Foo(100,500,Bar(900).toJson()).toJson()

You would want it to be transparent and be able to do

foo = Foo(100, 500, Bar(900))
json_data = foo.to_json()

Otherwise you have weird things, like you are initializing Foo with the serialized Bar object, instead of the actual object, just so you can serialize it. This will fail as soon as your initializer does anything (except setting properties) with the arguments it is passed.

I would consider implementing a custom JSON Encode/Decoder:

class Foo:
    def __init__(self, x, y, bar):
        self.x =x
        self.y = y
        self.bar = bar #Second class object is here

class Bar:
    def __init__(self, z):
        self.z = z
import json

class ClassJSONEncoder(json.JSONEncoder):
    def default(self, obj):
        if hasattr(obj, "__dict__"):
            return {"__class__": obj.__class__.__name__, **obj.__dict__}
        # Let the base class default method raise the TypeError
        return json.JSONEncoder.default(self, obj)

def as_class(dct):
    if '__class__' in dct:
        cls_name = dct.pop("__class__")
        cls = vars()[cls_name]
        return cls(**dct)
    return dct

Now, if you want to, you can add a JSONSerializable mix-in:

class JSONSerializable:
    def to_json(self):
        return json.dumps(self, cls=ClassJSONEncoder)

    @classmethod
    def from_json(cls, s):
        self = json.loads(s, object_hook=as_class)
        assert isinstance(self, cls)
        return self

So you can directly inherit from this:

class Foo(JSONSerializable):
    ...

class Bar(JSONSerializable):
    ...

Foo(100, 200, Bar(900)).to_json()
# '{"__class__": "Foo", "x": 100, "y": 200, "bar": {"__class__": "Bar", "z": 900}}'
Foo.from_json(Foo(100, 200, Bar(900)).to_json())
# <__main__.Foo at 0x7effb1d86e48>
# with {'x': 100, 'y': 200, 'bar': {'z': 900}}

Although this is maybe still not the best implementation, because Foo.from_json suggests that you get back a Foo object, while this serialization relies on the correct "__class__" key for the class name (although I added a check that this is the case).

This also does not deal with positional-only arguments. It also requires, just like your code, the __dict__ to be necessary and sufficient for creating a new object.

However, this approach has the added advantage that it can override the to_json and from_json methods, should you need to, while covering the base cases quite nicely, IMO.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I was looking for something like this. Thanks for this broad answer. \$\endgroup\$ – Yunus Jul 7 at 4:43
2
\$\begingroup\$

There's a library for this dataclasses-json. Whilst it's a pretty poor library the internal code is bad, there's few tests, the documentation is quite small and the design is starting to suffer from these poor decisions. It works, and for your code it is good enough.

You should be able to see that all serialization and deserialization is performed automatically. This is good as then you can focus on using the objects rather than converting to and from them.

from __future__ import annotations

from dataclasses import dataclass

from dataclasses_json import dataclass_json


@dataclass_json
@dataclass
class Foo:
    x: int
    y: int
    bar: Bar


@dataclass_json
@dataclass
class Bar:
    z: int


raw = '{"x": 100, "y": 500, "bar": {"z": 900}}'
obj = Foo.from_json(raw)
assert obj == Foo(100, 500, Bar(900))
assert obj.to_json() == raw
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thanks for this information. So if you could reply me with yes/no, I can go with my code implementation, right? As I am not going to implement any library which might have poor documentation and I may fall in trouble. \$\endgroup\$ – Yunus Jul 6 at 9:49
  • \$\begingroup\$ @Yunus Your comment does not make sense to me. \$\endgroup\$ – user226435 Jul 6 at 9:50

Not the answer you're looking for? Browse other questions tagged or ask your own question.