diff --git a/fedot/core/operations/operation.py b/fedot/core/operations/operation.py index 3fd5017b80..ebe4397507 100644 --- a/fedot/core/operations/operation.py +++ b/fedot/core/operations/operation.py @@ -1,5 +1,5 @@ from abc import abstractmethod -from typing import Optional, Union +from typing import Optional, Union, Dict, Any from fedot.core.data.data import InputData, OutputData from fedot.core.log import default_log @@ -7,9 +7,10 @@ from fedot.core.operations.operation_parameters import OperationParameters from fedot.core.repository.operation_types_repository import OperationMetaInfo from fedot.core.repository.tasks import Task, TaskTypesEnum, compatible_task_types -from fedot.core.utils import DEFAULT_PARAMS_STUB +from fedot.core.serializers.serializer import register_serializable +@register_serializable class Operation: """Base class for operations in nodes. Operations could be machine learning (or statistical) models or data operations @@ -151,6 +152,14 @@ def assign_tabular_column_types(output_data: OutputData, output_mode: str) -> Ou def __str__(self): return f'{self.operation_type}' + def to_json(self) -> Dict[str, Any]: + """Serializes object and ignores unrelevant fields.""" + return { + k: v + for k, v in sorted(vars(self).items()) + if k not in ['log', 'operations_repo', '_eval_strategy', 'fitted_operation'] + } + def _eval_strategy_for_task(operation_type: str, current_task_type: TaskTypesEnum, operations_repo): diff --git a/fedot/core/pipelines/node.py b/fedot/core/pipelines/node.py index b8da10047f..a67422dcd5 100644 --- a/fedot/core/pipelines/node.py +++ b/fedot/core/pipelines/node.py @@ -13,9 +13,11 @@ from fedot.core.operations.operation import Operation from fedot.core.optimisers.timer import Timer from fedot.core.repository.operation_types_repository import OperationTypesRepository +from fedot.core.serializers.serializer import register_serializable from fedot.core.utils import DEFAULT_PARAMS_STUB +@register_serializable @dataclass class NodeMetadata: """Dataclass. :class:`Node` metadata diff --git a/fedot/core/serializers/__init__.py b/fedot/core/serializers/__init__.py index bb5b64f3a2..347c9cc4e9 100644 --- a/fedot/core/serializers/__init__.py +++ b/fedot/core/serializers/__init__.py @@ -1 +1,2 @@ from .serializer import CLASS_PATH_KEY, INSTANCE_OR_CALLABLE, MODULE_X_NAME_DELIMITER, Serializer +from .any_serialization import any_to_json, any_from_json diff --git a/fedot/core/serializers/coders/any_serialization.py b/fedot/core/serializers/any_serialization.py similarity index 63% rename from fedot/core/serializers/coders/any_serialization.py rename to fedot/core/serializers/any_serialization.py index c58117ff6f..1851303b5e 100644 --- a/fedot/core/serializers/coders/any_serialization.py +++ b/fedot/core/serializers/any_serialization.py @@ -1,15 +1,13 @@ from copy import deepcopy from inspect import signature -from typing import Any, Dict, Type +from typing import Any, Dict, Type, TypeVar, Callable -from .. import INSTANCE_OR_CALLABLE, Serializer + +INSTANCE_OR_CALLABLE = TypeVar('INSTANCE_OR_CALLABLE', object, Callable) def any_to_json(obj: INSTANCE_OR_CALLABLE) -> Dict[str, Any]: - return { - **{k: v for k, v in sorted(vars(obj).items()) if not _is_log_var(k)}, - **Serializer.dump_path_to_obj(obj) - } + return {k: v for k, v in sorted(vars(obj).items()) if not _is_log_var(k)} def any_from_json(cls: Type[INSTANCE_OR_CALLABLE], json_obj: Dict[str, Any]) -> INSTANCE_OR_CALLABLE: diff --git a/fedot/core/serializers/coders/__init__.py b/fedot/core/serializers/coders/__init__.py index 6d032462e1..7290506479 100644 --- a/fedot/core/serializers/coders/__init__.py +++ b/fedot/core/serializers/coders/__init__.py @@ -1,9 +1,7 @@ # flake8: noqa -from .any_serialization import any_from_json, any_to_json from .enum_serialization import enum_from_json, enum_to_json from .graph_node_serialization import graph_node_to_json from .graph_serialization import graph_from_json -from .operation_serialization import operation_to_json from .opt_history_serialization import opt_history_from_json, opt_history_to_json from .parent_operator_serialization import parent_operator_from_json, parent_operator_to_json from .uuid_serialization import uuid_from_json, uuid_to_json diff --git a/fedot/core/serializers/coders/enum_serialization.py b/fedot/core/serializers/coders/enum_serialization.py index 6c0263af95..9e2d2ba270 100644 --- a/fedot/core/serializers/coders/enum_serialization.py +++ b/fedot/core/serializers/coders/enum_serialization.py @@ -1,14 +1,9 @@ from enum import Enum from typing import Any, Dict, Type -from .. import Serializer - def enum_to_json(obj: Enum) -> Dict[str, Any]: - return { - 'value': obj.value, - **Serializer.dump_path_to_obj(obj) - } + return { 'value': obj.value } def enum_from_json(cls: Type[Enum], json_obj: Dict[str, Any]) -> Enum: diff --git a/fedot/core/serializers/coders/graph_node_serialization.py b/fedot/core/serializers/coders/graph_node_serialization.py index edea7a40ec..7a838d25cc 100644 --- a/fedot/core/serializers/coders/graph_node_serialization.py +++ b/fedot/core/serializers/coders/graph_node_serialization.py @@ -1,7 +1,7 @@ from typing import Any, Dict from fedot.core.dag.linked_graph_node import LinkedGraphNode -from . import any_to_json +from .. import any_to_json def graph_node_to_json(obj: LinkedGraphNode) -> Dict[str, Any]: diff --git a/fedot/core/serializers/coders/objective_serialization.py b/fedot/core/serializers/coders/objective_serialization.py index 9ddff4d78c..af02f29473 100644 --- a/fedot/core/serializers/coders/objective_serialization.py +++ b/fedot/core/serializers/coders/objective_serialization.py @@ -2,7 +2,7 @@ from fedot.core.optimisers.objective import Objective from fedot.core.optimisers.objective.metrics_objective import MetricsObjective -from fedot.core.serializers.coders import any_from_json +from fedot.core.serializers.any_serialization import any_from_json def objective_from_json(cls: Type[Objective], json_obj: Dict[str, Any]) -> Objective: diff --git a/fedot/core/serializers/coders/operation_serialization.py b/fedot/core/serializers/coders/operation_serialization.py deleted file mode 100644 index 80b5ea6ab5..0000000000 --- a/fedot/core/serializers/coders/operation_serialization.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Any, Dict - -from fedot.core.operations.operation import Operation - -from . import any_to_json - - -def operation_to_json(obj: Operation) -> Dict[str, Any]: - """ - Uses regular serialization but excludes "operations_repo" field cause it has no any important info about class - """ - return { - k: v - for k, v in any_to_json(obj).items() - if k not in ['operations_repo', '_eval_strategy', 'fitted_operation'] - } diff --git a/fedot/core/serializers/coders/opt_history_serialization.py b/fedot/core/serializers/coders/opt_history_serialization.py index b8980ffd99..d7dd808489 100644 --- a/fedot/core/serializers/coders/opt_history_serialization.py +++ b/fedot/core/serializers/coders/opt_history_serialization.py @@ -4,7 +4,7 @@ from fedot.core.optimisers.graph import OptGraph from fedot.core.optimisers.opt_history_objects.individual import Individual from fedot.core.optimisers.opt_history_objects.opt_history import OptHistory -from . import any_from_json, any_to_json +from .. import any_from_json, any_to_json MISSING_INDIVIDUAL_ARGS = { 'metadata': {'MISSING_INDIVIDUAL': 'This individual could not be restored during `OptHistory.load()`'} diff --git a/fedot/core/serializers/coders/parent_operator_serialization.py b/fedot/core/serializers/coders/parent_operator_serialization.py index c8cd2e3d07..a109221584 100644 --- a/fedot/core/serializers/coders/parent_operator_serialization.py +++ b/fedot/core/serializers/coders/parent_operator_serialization.py @@ -1,7 +1,7 @@ from typing import Any, Dict, Type from fedot.core.optimisers.opt_history_objects.parent_operator import ParentOperator -from . import any_from_json, any_to_json +from .. import any_from_json, any_to_json def parent_operator_to_json(obj: ParentOperator) -> Dict[str, Any]: diff --git a/fedot/core/serializers/coders/uuid_serialization.py b/fedot/core/serializers/coders/uuid_serialization.py index 0f125bf410..617851f642 100644 --- a/fedot/core/serializers/coders/uuid_serialization.py +++ b/fedot/core/serializers/coders/uuid_serialization.py @@ -1,14 +1,9 @@ from typing import Any, Dict, Type from uuid import UUID -from .. import Serializer - def uuid_to_json(obj: UUID) -> Dict[str, Any]: - return { - 'hex': obj.hex, - **Serializer.dump_path_to_obj(obj) - } + return { 'hex': obj.hex } def uuid_from_json(cls: Type[UUID], json_obj: Dict[str, Any]) -> UUID: diff --git a/fedot/core/serializers/serializer.py b/fedot/core/serializers/serializer.py index f15df02b3b..29de81afa1 100644 --- a/fedot/core/serializers/serializer.py +++ b/fedot/core/serializers/serializer.py @@ -5,13 +5,14 @@ from json import JSONDecoder, JSONEncoder from typing import Any, Callable, Dict, Optional, Type, TypeVar, Union -from fedot.core.dag.linked_graph_node import LinkedGraphNode -from fedot.core.optimisers.fitness.fitness import Fitness -from fedot.core.optimisers.objective.objective import Objective -from fedot.core.pipelines.node import NodeMetadata +# NB: at the end of module init happens registration of default class coders -MODULE_X_NAME_DELIMITER = '/' INSTANCE_OR_CALLABLE = TypeVar('INSTANCE_OR_CALLABLE', object, Callable) +EncodeCallable = Callable[[INSTANCE_OR_CALLABLE], Dict[str, Any]] +DecodeCallable = Callable[[Type[INSTANCE_OR_CALLABLE], Dict[str, Any]], INSTANCE_OR_CALLABLE] + + +MODULE_X_NAME_DELIMITER = '/' CLASS_PATH_KEY = '_class_path' # Mapping between class paths for backward compatibility for renamed/moved classes @@ -39,55 +40,104 @@ class Serializer(JSONEncoder, JSONDecoder): CODERS_BY_TYPE = {} + __default_coders_initialized = False + def __init__(self, *args, **kwargs): for base_class, coder_name in [(JSONEncoder, 'default'), (JSONDecoder, 'object_hook')]: base_kwargs = {k: kwargs[k] for k in kwargs.keys() & signature(base_class.__init__).parameters} base_kwargs[coder_name] = getattr(self, coder_name) base_class.__init__(self, **base_kwargs) - if not Serializer.CODERS_BY_TYPE: - from uuid import UUID - - from fedot.core.dag.graph import Graph - from fedot.core.operations.operation import Operation - from fedot.core.optimisers.opt_history_objects.individual import Individual - from fedot.core.optimisers.opt_history_objects.opt_history import OptHistory - from fedot.core.optimisers.opt_history_objects.parent_operator import ParentOperator - from fedot.core.utilities.data_structures import ComparableEnum - - from .coders import ( - any_from_json, - any_to_json, - enum_from_json, - enum_to_json, - graph_from_json, - graph_node_to_json, - operation_to_json, - opt_history_from_json, - opt_history_to_json, - objective_from_json, - parent_operator_from_json, - parent_operator_to_json, - uuid_from_json, - uuid_to_json - ) - - _to_json = Serializer._to_json - _from_json = Serializer._from_json - basic_serialization = {_to_json: any_to_json, _from_json: any_from_json} - Serializer.CODERS_BY_TYPE = { - Fitness: basic_serialization, - Individual: basic_serialization, - NodeMetadata: basic_serialization, - LinkedGraphNode: {_to_json: graph_node_to_json, _from_json: any_from_json}, - Graph: {_to_json: any_to_json, _from_json: graph_from_json}, - Operation: {_to_json: operation_to_json, _from_json: any_from_json}, - OptHistory: {_to_json: opt_history_to_json, _from_json: opt_history_from_json}, - Objective: {_to_json: any_to_json, _from_json: objective_from_json}, - ParentOperator: {_to_json: parent_operator_to_json, _from_json: parent_operator_from_json}, - UUID: {_to_json: uuid_to_json, _from_json: uuid_from_json}, - ComparableEnum: {_to_json: enum_to_json, _from_json: enum_from_json}, - } + if not self.__default_coders_initialized: + Serializer._register_default_coders() + self.__default_coders_initialized = True + + @staticmethod + def _register_default_coders(): + from uuid import UUID + + from fedot.core.dag.graph import Graph + from fedot.core.dag.linked_graph_node import LinkedGraphNode + from fedot.core.optimisers.opt_history_objects.individual import Individual + from fedot.core.optimisers.opt_history_objects.opt_history import OptHistory + from fedot.core.optimisers.opt_history_objects.parent_operator import ParentOperator + from fedot.core.optimisers.fitness.fitness import Fitness + from fedot.core.optimisers.objective.objective import Objective + from fedot.core.utilities.data_structures import ComparableEnum + + from .any_serialization import any_from_json, any_to_json + + from .coders import ( + enum_from_json, + enum_to_json, + graph_from_json, + graph_node_to_json, + opt_history_from_json, + opt_history_to_json, + objective_from_json, + parent_operator_from_json, + parent_operator_to_json, + uuid_from_json, + uuid_to_json + ) + + _to_json = Serializer._to_json + _from_json = Serializer._from_json + basic_serialization = {_to_json: any_to_json, _from_json: any_from_json} + + default_coders = { + Fitness: basic_serialization, + Individual: basic_serialization, + LinkedGraphNode: {_to_json: graph_node_to_json, _from_json: any_from_json}, + Graph: {_to_json: any_to_json, _from_json: graph_from_json}, + OptHistory: {_to_json: opt_history_to_json, _from_json: opt_history_from_json}, + Objective: {_to_json: any_to_json, _from_json: objective_from_json}, + ParentOperator: {_to_json: parent_operator_to_json, _from_json: parent_operator_from_json}, + UUID: {_to_json: uuid_to_json, _from_json: uuid_from_json}, + ComparableEnum: {_to_json: enum_to_json, _from_json: enum_from_json}, + } + Serializer.CODERS_BY_TYPE.update(default_coders) + + @staticmethod + def register_coders(cls: Type[INSTANCE_OR_CALLABLE], + to_json: Optional[EncodeCallable[INSTANCE_OR_CALLABLE]] = None, + from_json: Optional[DecodeCallable[INSTANCE_OR_CALLABLE]] = None, + overwrite: bool = False, + ) -> Type[INSTANCE_OR_CALLABLE]: + """Registers classes as json-serializable so that they can be used by `Serializer`. + + Supports 3 alternative usages: + + - Default serialization is used (functions `any_to_json` and `any_from_json`). + - Custom serialization functions `to_json` & `from_json` are provided explicitly as arguments. + - Custom serialization functions `to_json` & `from_json` are defined in the class. + + Args: + cls: registered class + to_json: custom encoding function that returns json Dict. + Optional, if None then default is used. + from_json: custom decoding function that returns class given a Dict. + Optional, if None then default is used. + overwrite: flag that allows to overwrite existing registered coders. + False by default: method raises AttributeError in attempt to overwrite coders. + + Returns: + cls: class that is registered in serializer + """ + if cls is None: + raise TypeError('Class must not be None.') + from .any_serialization import any_from_json, any_to_json + + # get provided coders or coders defined in the class itself or default universal coders + coders = {Serializer._to_json: to_json or getattr(cls, Serializer._to_json, any_to_json), + Serializer._from_json: from_json or getattr(cls, Serializer._from_json, any_from_json)} + + if cls not in Serializer.CODERS_BY_TYPE or overwrite: + Serializer.CODERS_BY_TYPE[cls] = coders + else: + raise AttributeError(f'Object {cls} already has serializer coders registered.') + + return cls @staticmethod def _get_field_checker(obj: Union[INSTANCE_OR_CALLABLE, Type[INSTANCE_OR_CALLABLE]]) -> Callable[..., bool]: @@ -141,7 +191,10 @@ def default(self, obj: INSTANCE_OR_CALLABLE) -> Dict[str, Any]: return Serializer.dump_path_to_obj(obj) base_type = Serializer._get_base_type(obj) if base_type is not None: - return Serializer._get_coder_by_type(base_type, Serializer._to_json)(obj) + encoded = Serializer._get_coder_by_type(base_type, Serializer._to_json)(obj) + if CLASS_PATH_KEY not in encoded: + encoded.update(Serializer.dump_path_to_obj(obj)) + return encoded return JSONEncoder.default(self, obj) @@ -161,7 +214,12 @@ def _get_class(class_path: str) -> Type[INSTANCE_OR_CALLABLE]: obj_cls = getattr(obj_cls, sub) return obj_cls - def object_hook(self, json_obj: Dict[str, Any]) -> Union[INSTANCE_OR_CALLABLE, dict]: + @staticmethod + def _is_bound_method(method: Callable) -> bool: + return hasattr(method, '__self__') + + @staticmethod + def object_hook(json_obj: Dict[str, Any]) -> Union[INSTANCE_OR_CALLABLE, dict]: """ Decodes every JSON-object to python class/func object or just returns dict @@ -175,22 +233,28 @@ def object_hook(self, json_obj: Dict[str, Any]) -> Union[INSTANCE_OR_CALLABLE, d del json_obj[CLASS_PATH_KEY] base_type = Serializer._get_base_type(obj_cls) if isclass(obj_cls) and base_type is not None: - return Serializer._get_coder_by_type(base_type, Serializer._from_json)(obj_cls, json_obj) + coder = Serializer._get_coder_by_type(base_type, Serializer._from_json) + # call with a right num of arguments + if Serializer._is_bound_method(coder): + return coder(json_obj) + else: + return coder(obj_cls, json_obj) elif isfunction(obj_cls) or ismethod(obj_cls): return obj_cls raise TypeError(f'Parsed obj_cls={obj_cls} is not serializable, but should be') return json_obj -def default_save(obj: Any, json_file_path: Union[str, os.PathLike] = None) -> Optional[str]: +def default_save(obj: Any, json_file_path: Optional[Union[str, os.PathLike]] = None) -> Optional[str]: """ Default save to json using Serializer """ if json_file_path is None: return json.dumps(obj, indent=4, cls=Serializer) with open(json_file_path, mode='w') as json_file: json.dump(obj, json_file, indent=4, cls=Serializer) + return None -def default_load(json_str_or_file_path: Union[str, os.PathLike] = None) -> Any: +def default_load(json_str_or_file_path: Union[str, os.PathLike]) -> Any: """ Default load from json using Serializer """ def load_as_file_path(): with open(json_str_or_file_path, mode='r') as json_file: @@ -206,3 +270,49 @@ def load_as_json_str(): return load_as_json_str() except json.JSONDecodeError: return load_as_file_path() + + +def register_serializable(cls: Optional[Type[INSTANCE_OR_CALLABLE]] = None, + to_json: Optional[EncodeCallable] = None, + from_json: Optional[DecodeCallable] = None, + overwrite: bool = False, + add_save_load: bool = False, + ) -> Type[INSTANCE_OR_CALLABLE]: + """Decorator for registering classes as json-serializable. + Relies on :py:class:`fedot.core.serializers.serializer.Serializer.register_coders`. + Optionally adds `save` and `load` methods to the class for json (de)serialization. + + Args: + cls: decorated class + to_json: custom encoding function that returns json Dict. + Optional, if None then default is used. + from_json: custom decoding function that returns class given a Dict. + Optional, if None then default is used. + overwrite: flag that allows to overwrite existing registered coders. + False by default: method raises AttributeError in attempt to overwrite coders. + add_save_load: if True, then `save` and `load` methods are added to the class + that (de)serialize the class (from)to the file using `Serializer`. + See `default_save` & `default_load` functions. + + Returns: + cls: class that is registered in serializer + """ + _save = 'save' + _load = 'load' + + def make_serializable(cls): + Serializer.register_coders(cls, to_json, from_json, overwrite) + if add_save_load: + if hasattr(cls, _save) or hasattr(cls, _load): + raise ValueError(f'Class {cls} already have `save` and/or `load` methods, can not overwrite them.') + setattr(cls, _save, default_save) + setattr(cls, _load, default_load) + return cls + + # See if we're being called as @decorator() (with parens). + if cls is None: + # We're called with parens. + return make_serializable + + # We're called as `@decorator` without parens. + return make_serializable(cls) diff --git a/test/unit/serialization/conftest.py b/test/unit/serialization/conftest.py index b9691ace2b..adcd632ee5 100644 --- a/test/unit/serialization/conftest.py +++ b/test/unit/serialization/conftest.py @@ -2,19 +2,16 @@ import pytest -from fedot.core.serializers import Serializer +from fedot.core.serializers import Serializer, any_to_json, any_from_json from fedot.core.serializers.coders import ( - any_from_json, - any_to_json, enum_from_json, enum_to_json, graph_from_json, graph_node_to_json, - operation_to_json, uuid_from_json, uuid_to_json ) -from .mocks.serialization_mocks import MockGraph, MockNode, MockOperation +from .mocks.serialization_mocks import MockGraph, MockNode, MockOperation, operation_to_json from .shared_data import TestClass, TestEnum, TestSerializableClass diff --git a/test/unit/serialization/mocks/serialization_mocks.py b/test/unit/serialization/mocks/serialization_mocks.py index 68e3b88739..dba07d006e 100644 --- a/test/unit/serialization/mocks/serialization_mocks.py +++ b/test/unit/serialization/mocks/serialization_mocks.py @@ -10,6 +10,17 @@ def __eq__(self, other): return self.operation_type == other.operation_type +def operation_to_json(self): + """ + Uses regular serialization but excludes "operations_repo" field cause it has no any important info about class + """ + return { + k: v + for k, v in sorted(vars(self).items()) + if k not in ['operations_repo'] + } + + class MockNode: def __init__(self, name: str, nodes_from: list = None): self.name = name diff --git a/test/unit/serialization/test_custom_serialize.py b/test/unit/serialization/test_custom_serialize.py new file mode 100644 index 0000000000..3c5aea4ede --- /dev/null +++ b/test/unit/serialization/test_custom_serialize.py @@ -0,0 +1,98 @@ +import json +from copy import deepcopy + +import pytest +from typing import Dict + +from fedot.core.serializers.serializer import register_serializable, Serializer, CLASS_PATH_KEY + + +class DataEq: + def __init__(self, data): + self.data = data + self._data = data + + def __eq__(self, other): + return (self.data == other.data and + self._data == other._data) + + +@register_serializable(add_save_load=True) +class DefaultSerializable(DataEq): + pass + + +custom_id = '0042' + + +def encode_custom(obj): + return {custom_id: obj.data} + + +def decode_custom(cls, _dict): + return cls(data=_dict[custom_id]) + + +@register_serializable(to_json=encode_custom, from_json=decode_custom, add_save_load=True) +class CustomSerializable(DataEq): + pass + + +@register_serializable(add_save_load=True) +class CustomSerializableWithMethods(DataEq): + pass + + def to_json(self) -> Dict: + return {custom_id: self.data} + + @classmethod + def from_json(cls, _dict): + return CustomSerializableWithMethods(_dict[custom_id]) + + +@pytest.mark.parametrize('obj', [DefaultSerializable(42), + CustomSerializable(666), + CustomSerializableWithMethods(777)]) +def test_serializable(obj): + dumped = json.dumps(obj, cls=Serializer) + loaded = json.loads(dumped, cls=Serializer) + + assert loaded == obj + + +@pytest.mark.parametrize('obj', [DefaultSerializable(42), + CustomSerializable(666), + CustomSerializableWithMethods(777)]) +def test_default_save_load(obj): + # test that have 'save' and 'load' methods added by default + assert hasattr(obj, 'save') + assert hasattr(obj, 'load') + assert obj.__class__.load(obj.save()) == obj + + +@pytest.mark.parametrize('obj', [CustomSerializableWithMethods(777)]) +def test_serializable_with_class_methods(obj): + dumped_srz = json.dumps(obj, cls=Serializer) + dumped_self = obj.to_json() + + assert isinstance(dumped_srz, str) + assert isinstance(dumped_self, dict) + + decoded_self = obj.from_json(dumped_self) + decoded_srz = json.loads(dumped_srz, cls=Serializer) + + assert decoded_self == decoded_srz == obj + + +@pytest.mark.parametrize('obj', [CustomSerializable(666)]) +def test_serializable_custom(obj): + dumped_srz = json.dumps(obj, cls=Serializer) + dumped_self = encode_custom(obj) + + assert isinstance(dumped_srz, str) + assert isinstance(dumped_self, dict) + + decoded_self = decode_custom(obj.__class__, dumped_self) + decoded_srz = json.loads(dumped_srz, cls=Serializer) + + assert decoded_self == decoded_srz == obj