Home > Blockchain >  Serializing nested classes
Serializing nested classes

Time:11-30

I'm trying to create a custom JSON encoder for a dataclass, but that class is actually embedded in another class, with the top class being serialized. My class definitions are like so:

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class Foo():
    foo_member: int = 1

    @property
    def a_property(self):
        return self.foo_member 1

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class Bar():
    foo_list: List[Foo] = field(default_factory=list)

My whole test code is:

from dataclasses import dataclass, field, asdict, is_dataclass
from typing import List

from json import JSONEncoder

from pprint import pprint

class FooJsonEncoder(JSONEncoder):
    '''
    This should be used exclusively for encoding the ELF metadata as KDataFormat
    is treated differently here.
    '''
    def custom(self, x):
        print(f'custom type {type(x)}')

        if isinstance(x, list):
            print(f'here {dict(x)}')
            pprint(x)

        if isinstance(x, Foo):
            d = asdict(x)
            d['a_property'] = getattr(x, 'a_property')
            return d
        elif is_dataclass(x):
            return asdict(x)
        return dict(x)

    def default(self, o):
        print(f'default type {type(o)}')
        if isinstance(o, Foo):
            d = asdict(o)
            d['a_property'] = getattr(o, 'a_property')
            return d
        elif is_dataclass(o):
            return asdict(o, dict_factory=self.custom)

        return super(FooJsonEncoder, self).default(o)


@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class Foo():
    foo_member: int = 1

    @property
    def a_property(self):
        return self.foo_member 1

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class Bar():
    foo_list: List[Foo] = field(default_factory=list)


def main():
    foo1 = Foo(1)
    foo2 = Foo(2)
    assert 2 == foo1.a_property
    assert 3 == foo2.a_property

    bar = Bar(foo_list=[foo1, foo2])

    print(FooJsonEncoder().encode(bar))

if __name__ == "__main__":
    main()

When I run it, I get

default type <class '__main__.Bar'>
custom type <class 'list'>
here {'foo_member': 1}
[('foo_member', 1)]
custom type <class 'list'>
here {'foo_member': 2}
[('foo_member', 2)]
custom type <class 'list'>
here {'foo_list': [{'foo_member': 1}, {'foo_member': 2}]}
[('foo_list', [{'foo_member': 1}, {'foo_member': 2}])]
{"foo_list": [{"foo_member": 1}, {"foo_member": 2}]}

My FooJsonEncoder.default is being called once by main. It's interesting that FooJsonEncoder.custom is being called with split lists instead of of list of two Foo objects:

custom type <class 'list'>
here {'foo_member': 1}
[('foo_member', 1)]
custom type <class 'list'>
here {'foo_member': 2}
[('foo_member', 2)]

Then gets called with a two-member list, but already converted to dict:

custom type <class 'list'>
here {'foo_list': [{'foo_member': 1}, {'foo_member': 2}]}
[('foo_list', [{'foo_member': 1}, {'foo_member': 2}])]
{"foo_list": [{"foo_member": 1}, {"foo_member": 2}]}

Once return dict(x) gets called in custom, then I can't use a custom conversion for the nested class.

How can pass a custom JSON serializer when the class is nested?

Thank you.

CodePudding user response:

I think the problem is that asdict is recursive but doesn't give you access to the steps in between. So once you hit bar asdict takes over and serializes all the dataclasses. There might be a way to make a_property a field and side-step this issue.

But I just manually converted the dataclasses to a dictionary which let me add the extra field. You just have to be aware that asdict actually will copy lists and this does not do that but it is not needed because it is all being serialized to a string.

from dataclasses import (
    dataclass,
    field,
    is_dataclass,
    fields)
from typing import List

from json import JSONEncoder
from pprint import pformat

class FooJsonEncoder(JSONEncoder):

    def default(self, obj):
        print ('serializing {}'.format(pformat(obj)))
        if is_dataclass(obj):
            print ('    is dataclass')
            fieldnames = [f.name for f in fields(obj)]
            if isinstance(obj, Foo):
                print ('    is Foo')
                fieldnames.append('a_property')
            d = dict([(name, getattr(obj, name)) for name in fieldnames])
            print('    serialized to {}'.format(pformat(d)))
            return d
        else:
            pprint ('    is not dataclass')
            res = super(FooJsonEncoder, self).default(obj)
            print('    serialized to {}'.format(pformat(res)))
            return res


@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class Foo():
    foo_member: int = 1

    @property
    def a_property(self):
        return self.foo_member 1

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class Bar():
    foo_list: List[Foo] = field(default_factory=list)


def main():
    foo1 = Foo(1)
    foo2 = Foo(2)
    assert 2 == foo1.a_property
    assert 3 == foo2.a_property

    bar = Bar(foo_list=[foo1, foo2])

    print(FooJsonEncoder().encode(bar))

if __name__ == "__main__":
    main()

  • Related