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()