Skip to content

Instantly share code, notes, and snippets.

@RHDZMOTA
Last active December 2, 2021 17:32
Show Gist options
  • Save RHDZMOTA/3b50b284d1892e4ac1bcffcca7dab135 to your computer and use it in GitHub Desktop.
Save RHDZMOTA/3b50b284d1892e4ac1bcffcca7dab135 to your computer and use it in GitHub Desktop.
import inspect
import json
from typing import Dict, TypeVar
SerializableDataType = TypeVar(
"SerializableDataType",
bound="Serializable"
)
class Serializable:
default_cls_encoder = json.JSONEncoder
default_cls_decoder = json.JSONDecoder
@classmethod
def from_json(cls, content: str, **kwargs) -> SerializableDataType:
kwargs["cls"] = kwargs.pop("json_cls", cls.default_cls_decoder)
instance_kwargs = json.loads(content, **kwargs)
return cls(**instance_kwargs)
@classmethod
def from_file(cls, filename: str, **kwargs) -> SerializableDataType:
with open(filename, "r") as file:
return cls.from_json(content=file.read(), **kwargs)
@classmethod
def _get_init_arglist(cls):
return inspect.getfullargspec(cls).args
def to_dict(self, error_if_not_exists: bool = False) -> Dict:
return {
arg: getattr(self, arg) if error_if_not_exists else \
getattr(self, arg, None)
for arg in self._get_init_arglist()
if arg != "self"
}
def to_json(self, **kwargs) -> str:
dictionary = self.to_dict()
kwargs["cls"] = kwargs.pop("json_cls", self.default_cls_encoder)
return json.dumps(dictionary, **kwargs)
@RHDZMOTA
Copy link
Author

RHDZMOTA commented Nov 15, 2021

Simple example using dataclasses:

from dataclasses import dataclass

from serializable_helper import Serializable

@dataclass
class ProgrammingLang(Serializable):
    name: str
    creation_year: int
        
    def is_younger(self, other: 'ProgrammingLang') -> bool:
        return self.creation_year > other.creation_year
# Instances
python = ProgrammingLang("Python", 1991)
rust = ProgrammingLang("Rust", 2010)
scala = ProgrammingLang("Scala", 2001)

print(python, rust, scala, sep="\n")

Instances should be now serializable:

message = "Serialize 'ProgrammingLang' instances into json strings:\n"
print(message, python.to_json(), scala.to_json(), rust.to_json(), sep="\n")

You should also be able to decode json strings:

java_json = """{"name": "Java", "creation_year": 1995}"""

java = ProgrammingLang.from_json(java_json)

message = "Create a 'ProgrammingLang' instance from a json string:\n"
print(message, java, sep="\n")
assert java.is_younger(other=python)
assert isinstance(java, ProgrammingLang)

@RHDZMOTA
Copy link
Author

If you have json-incompatible datatypes, you'll need to provide companion decoders & encoders classes.

Consider the a simple human class with:

  • name (str): the name is a json-compatible datatype.
  • birthdate (datetime): the birthdate is json-incompatile; we need to serialize (encode) the string before transforming to JSON and decode when creating a human instance.

You have two options:

  • Use a regular string instread of a datetime instance.
  • Create the encoders/decoders.

Let's take a look at the second option:

import json
import datetime as dt
import dateutil

from typing import Any, Dict, Union

from serializable_helper import Serializable


class HumanDecoder(json.JSONDecoder): 
    
    def __init__(self, *args, **kwargs):
        super().__init__(object_hook=self.object_hook, *args, **kwargs)
    
    def object_hook(self, obj: Dict) -> Dict:
        human_attributes = set(Human._get_init_arglist())
        object_keys = set(obj.keys())
        # If not all human attributes match; return dict object
        if human_attributes - object_keys - {"self"}:
            return obj
        # Return human-ready kwargs
        return {
            "name": obj["name"],
            "birthdate": dateutil.parser.parse(obj["birthdate"])
        }
    

class HumanEncoder(json.JSONEncoder):
    
    def default(self, obj: Any):
        if isinstance(obj, Human):
            return obj.to_json()
        if isinstance(obj, dt.datetime):
            return dt.datetime.strftime(obj, "%Y-%m-%d")
        return json.JSONEncoder.default(self, obj)

    
class Human(Serializable):
    default_cls_decoder = HumanDecoder
    default_cls_encoder = HumanEncoder

    def __init__(self, name: str, birthdate: dt.datetime):
        self.name = name
        self.birthdate = birthdate

    def __repr__(self) -> str:
        return f"{self.name}"

You should now be able to use the Human class with full serialization capabilities!

human_data = """
{
    "name": "Guido van Rossum",
    "birthdate": "1956-01-31"
}
"""

# Create a human instance from a valid json string
human_instance = Human.from_json(human_data)

# Validations
assert isinstance(human_instance, Human)
assert isinstance(human_instance.birthdate, dt.datetime)

print(human_instance, human_instance.to_json(), sep="\n")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment