I LOVE the python module json, and I love json.dumps(); however, I hate the fact that you need to pass additional arguments to it if you want to extend its behaviour.
I am strong believer that classes should take care of themselves, so I created the class Serialisable
that goes like this:
class Serialisable(dict[str, Any]): def __len__(self) -> int: return len(self.items()) def __str__(self) -> str: return str(dict(self.items())) def __getitem__(self, __key: str) -> Any: return getattr(self, __key) def __setitem__(self, __key: str, __value: Any) -> None: vars(self)[__key] = __value def __delitem__(self, __key: str) -> None: variable = getattr(self, __key) del variable def __contains__(self, __key: str) -> bool: return __key in vars(self) def __iter__(self) -> Iterator[str]: return self.keys().__iter__() def keys(self) -> list[str]: return [keys for keys, _ in self.items()] def values(self) -> list[Any]: return [values for _, values in self.items()] def items(self) -> list[tuple[str, Any]]: result: list[tuple[str, Any]] = [] for name, value in vars(self).items(): variable = getattr(self, name) if isinstance(variable, Serialisable): pass elif 'builtin' not in variable.__class__.__module__: continue result.append((name, value)) return result
Basically, if you have a sub class of Serialisable
, all of the instance variables (that are serialisable) will appear as part of a dictionary:
@dataclassclass subSerial(Serialisable): boolean: bool = True real: float = -1.0@dataclassclass NonSerial: string: str = 'Miguel'class mainSerial(Serialisable): def __init__(self) -> None: self.string: str = 'Calgary' self.integer: int = 53 self.subClass: subSerial = subSerial() self.name: dict[str, str] = {'King': 'Charles'} self.discard: NonSerial = NonSerial()print(f"{serial}")
{'string': 'Calgary', 'integer': 53, 'subClass': subSerial(boolean=True, real=-1.0), 'name': {'King': 'Charles'}}
If I run this code print(f"JSON indent = {json.dumps(serial, sort_keys=True, indent=4)}")
I get this pretty result:
JSON indent = {"integer": 28,"name": {"King": "Charles" },"string": "Calgary","subClass": {"boolean": true,"real": -1.0 }}
If I remove the indent=4
, I get this:
JSON NON indent = {}
Long story short, it all boils down to how Python has implemented json.dumps()
and this piece of code on encoder.py
if (_one_shot and c_make_encoder is not None and self.indent is None): _iterencode = c_make_encoder( markers, self.default, _encoder, self.indent, self.key_separator, self.item_separator, self.sort_keys, self.skipkeys, self.allow_nan) else: _iterencode = _make_iterencode( markers, self.default, _encoder, self.indent, floatstr, self.key_separator, self.item_separator, self.sort_keys, self.skipkeys, _one_shot) return _iterencode(o, 0)
c_make_encoder
seems to be an external library function (written in C?) that apparently doesn't understand how custom dictionaries work!
So, here is(are) my question(s):
- Do you know what I am missing on my
Serialisable
class to make it act a like a full dictionary andjson.dumps()
serialises it properly? alternatively - Do you know where can I find the code of c_make_encoder?
(I have found this very useless code on _json.pyi
)
@finalclass make_encoder: @property def sort_keys(self) -> bool: ... @property def skipkeys(self) -> bool: ... @property def key_separator(self) -> str: ...[...]