123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- import json
- import logging
- from string import Template
- from typing import Any, Dict, Type, TypeVar, Union
- T = TypeVar("T", bound="JSONSerializable")
- # NOTE: Through inheritance, all of our classes should be children of JSONSerializable. (highest level)
- # NOTE: The @register_deserializable decorator should be added to all user facing child classes. (lowest level)
- def register_deserializable(cls: Type[T]) -> Type[T]:
- """
- A class decorator to register a class as deserializable.
- When a class is decorated with @register_deserializable, it becomes
- a part of the set of classes that the JSONSerializable class can
- deserialize.
- Deserialization is in essence loading attributes from a json file.
- This decorator is a security measure put in place to make sure that
- you don't load attributes that were initially part of another class.
- Example:
- @register_deserializable
- class ChildClass(JSONSerializable):
- def __init__(self, ...):
- # initialization logic
- Args:
- cls (Type): The class to be registered.
- Returns:
- Type: The same class, after registration.
- """
- JSONSerializable.register_class_as_deserializable(cls)
- return cls
- class JSONSerializable:
- """
- A class to represent a JSON serializable object.
- This class provides methods to serialize and deserialize objects,
- as well as save serialized objects to a file and load them back.
- """
- _deserializable_classes = set() # Contains classes that are whitelisted for deserialization.
- def serialize(self) -> str:
- """
- Serialize the object to a JSON-formatted string.
- Returns:
- str: A JSON string representation of the object.
- """
- try:
- return json.dumps(self, default=self._auto_encoder, ensure_ascii=False)
- except Exception as e:
- logging.error(f"Serialization error: {e}")
- return "{}"
- @classmethod
- def deserialize(cls, json_str: str) -> Any:
- """
- Deserialize a JSON-formatted string to an object.
- If it fails, a default class is returned instead.
- Note: This *returns* an instance, it's not automatically loaded on the calling class.
- Example:
- app = App.deserialize(json_str)
- Args:
- json_str (str): A JSON string representation of an object.
- Returns:
- Object: The deserialized object.
- """
- try:
- return json.loads(json_str, object_hook=cls._auto_decoder)
- except Exception as e:
- logging.error(f"Deserialization error: {e}")
- # Return a default instance in case of failure
- return cls()
- @staticmethod
- def _auto_encoder(obj: Any) -> Union[Dict[str, Any], None]:
- """
- Automatically encode an object for JSON serialization.
- Args:
- obj (Object): The object to be encoded.
- Returns:
- dict: A dictionary representation of the object.
- """
- if hasattr(obj, "__dict__"):
- dct = obj.__dict__.copy()
- for key, value in list(
- dct.items()
- ): # We use list() to get a copy of items to avoid dictionary size change during iteration.
- try:
- # Recursive: If the value is an instance of a subclass of JSONSerializable,
- # serialize it using the JSONSerializable serialize method.
- if isinstance(value, JSONSerializable):
- serialized_value = value.serialize()
- # The value is stored as a serialized string.
- dct[key] = json.loads(serialized_value)
- # Custom rules (subclass is not json serializable by default)
- elif isinstance(value, Template):
- dct[key] = {"__type__": "Template", "data": value.template}
- # Future custom types we can follow a similar pattern
- # elif isinstance(value, SomeOtherType):
- # dct[key] = {
- # "__type__": "SomeOtherType",
- # "data": value.some_method()
- # }
- # NOTE: Keep in mind that this logic needs to be applied to the decoder too.
- else:
- json.dumps(value) # Try to serialize the value.
- except TypeError:
- del dct[key] # If it fails, remove the key-value pair from the dictionary.
- dct["__class__"] = obj.__class__.__name__
- return dct
- raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
- @classmethod
- def _auto_decoder(cls, dct: Dict[str, Any]) -> Any:
- """
- Automatically decode a dictionary to an object during JSON deserialization.
- Args:
- dct (dict): The dictionary representation of an object.
- Returns:
- Object: The decoded object or the original dictionary if decoding is not possible.
- """
- class_name = dct.pop("__class__", None)
- if class_name:
- if not hasattr(cls, "_deserializable_classes"): # Additional safety check
- raise AttributeError(f"`{class_name}` has no registry of allowed deserializations.")
- if class_name not in {cl.__name__ for cl in cls._deserializable_classes}:
- raise KeyError(f"Deserialization of class `{class_name}` is not allowed.")
- target_class = next((cl for cl in cls._deserializable_classes if cl.__name__ == class_name), None)
- if target_class:
- obj = target_class.__new__(target_class)
- for key, value in dct.items():
- if isinstance(value, dict) and "__type__" in value:
- if value["__type__"] == "Template":
- value = Template(value["data"])
- # For future custom types we can follow a similar pattern
- # elif value["__type__"] == "SomeOtherType":
- # value = SomeOtherType.some_constructor(value["data"])
- default_value = getattr(target_class, key, None)
- setattr(obj, key, value or default_value)
- return obj
- return dct
- def save_to_file(self, filename: str) -> None:
- """
- Save the serialized object to a file.
- Args:
- filename (str): The path to the file where the object should be saved.
- """
- with open(filename, "w", encoding="utf-8") as f:
- f.write(self.serialize())
- @classmethod
- def load_from_file(cls, filename: str) -> Any:
- """
- Load and deserialize an object from a file.
- Args:
- filename (str): The path to the file from which the object should be loaded.
- Returns:
- Object: The deserialized object.
- """
- with open(filename, "r", encoding="utf-8") as f:
- json_str = f.read()
- return cls.deserialize(json_str)
- @classmethod
- def register_class_as_deserializable(cls, target_class: Type[T]) -> None:
- """
- Register a class as deserializable. This is a classmethod and globally shared.
- This method adds the target class to the set of classes that
- can be deserialized. This is a security measure to ensure only
- whitelisted classes are deserialized.
- Args:
- target_class (Type): The class to be registered.
- """
- cls._deserializable_classes.add(target_class)
|