Repository: microsoft/electionguard-python

Issue:
Add functional methods for deserialization
Serialization has functional methods that allow for parsing out to objects but we are missing the reverse to parse into objects. 

```python
def read_json_object(data: Any, generic_type: T) -> T:
    set_deserializers()
    return cast(T, load(data, generic_type))
```

Comments:

Patch:
diff --git a/src/electionguard/serializable.py b/src/electionguard/serializable.py
index ffd69ca..25ce0d5 100644
--- a/src/electionguard/serializable.py
+++ b/src/electionguard/serializable.py
@@ -1,7 +1,7 @@
 from dataclasses import dataclass
 from datetime import datetime
 from os import path
-from typing import Any, cast, TypeVar, Generic
+from typing import Any, cast, Type, TypeVar
 
 from jsons import (
     dump,
@@ -17,6 +17,7 @@ from jsons import (
     default_nonetype_deserializer,
 )
 
+S = TypeVar("S", bound="Serializable")
 T = TypeVar("T")
 
 JSON_FILE_EXTENSION: str = ".json"
@@ -28,7 +29,7 @@ KEYS_TO_REMOVE = ["from_json", "from_json_file", "from_json_object"]
 
 
 @dataclass
-class Serializable(Generic[T]):
+class Serializable:
     """
     Serializable class with methods to convert to json
     """
@@ -61,35 +62,29 @@ class Serializable(Generic[T]):
         write_json_file(self, file_name, file_path, strip_privates)
 
     @classmethod
-    def from_json(cls, data: str) -> T:
+    def from_json(cls: Type[S], data: str) -> S:
         """
         Deserialize the provided data string into the specified instance
         :param data: JSON string
         """
-        set_deserializers()
-        return cast(T, loads(data, cls))
+        return read_json(data, cls)
 
     @classmethod
-    def from_json_object(cls, data: object) -> T:
+    def from_json_object(cls: Type[S], data: object) -> S:
         """
         Deserialize the provided data object into the specified instance
         :param data: JSON object
         """
-        set_deserializers()
-        return cast(T, load(data, cls))
+        return read_json_object(data, cls)
 
     @classmethod
-    def from_json_file(cls, file_name: str, file_path: str = "") -> T:
+    def from_json_file(cls: Type[S], file_name: str, file_path: str = "") -> S:
         """
         Deserialize the provided file into the specified instance
         :param file_name: File name
         :param file_path: File path
         """
-        json_file_path: str = path.join(file_path, file_name + JSON_FILE_EXTENSION)
-        with open(json_file_path, READ) as json_file:
-            data = json_file.read()
-            target = cls.from_json(data)
-        return target
+        return read_json_file(cls, file_name, file_path)
 
 
 def _remove_key(obj: Any, key_to_remove: str) -> Any:
@@ -169,6 +164,44 @@ def write_json_file(
         json_file.write(write_json(object_to_write, strip_privates))
 
 
+def read_json(data: Any, class_out: Type[T]) -> T:
+    """
+    Deserialize json file to object
+    :param data: Json file data
+    :param class_out: Object type
+    :return: Deserialized object
+    """
+    set_deserializers()
+    return cast(T, loads(data, class_out))
+
+
+def read_json_object(data: Any, class_out: Type[T]) -> T:
+    """
+    Deserialize json file to object
+    :param data: Json file data
+    :param class_out: Object type
+    :return: Deserialized object
+    """
+    set_deserializers()
+    return cast(T, load(data, class_out))
+
+
+def read_json_file(class_out: Type[T], file_name: str, file_path: str = "") -> T:
+    """
+    Deserialize json file to object
+    :param class_out: Object type
+    :param file_name: File name
+    :param file_path: File path
+    :return: Deserialized object
+    """
+    set_deserializers()
+    json_file_path: str = path.join(file_path, file_name + JSON_FILE_EXTENSION)
+    with open(json_file_path, READ) as json_file:
+        data = json_file.read()
+        target: T = read_json(data, class_out)
+    return target
+
+
 def set_serializers() -> None:
     """Set serializers for jsons to use to cast specific classes"""
 


Test patch:
diff --git a/tests/test_serializable.py b/tests/test_serializable.py
index e64ab95..c4a2682 100644
--- a/tests/test_serializable.py
+++ b/tests/test_serializable.py
@@ -1,74 +1,93 @@
+from dataclasses import dataclass
 from unittest import TestCase
+from typing import Any, List, Optional
 from os import remove
 
 from electionguard.serializable import (
     set_deserializers,
     set_serializers,
+    read_json,
+    read_json_file,
+    read_json_object,
+    write_json,
     write_json_file,
     write_json_object,
-    write_json,
 )
 
 
+@dataclass
+class NestedModel:
+    """Nested model for testing"""
+
+    test: int
+    from_json_file: Optional[Any] = None
+
+
+@dataclass
+class DataModel:
+    """Data model for testing"""
+
+    test: int
+    nested: NestedModel
+    array: List[NestedModel]
+    from_json_file: Optional[Any] = None
+
+
+JSON_DATA: DataModel = DataModel(
+    test=1, nested=NestedModel(test=1), array=[NestedModel(test=1)]
+)
+EXPECTED_JSON_STRING = '{"array": [{"test": 1}], "nested": {"test": 1}, "test": 1}'
+EXPECTED_JSON_OBJECT = {
+    "test": 1,
+    "nested": {"test": 1},
+    "array": [{"test": 1}],
+}
+
+
 class TestSerializable(TestCase):
-    def test_write_json(self) -> None:
-        # Arrange
-        json_data = {
-            "from_json_file": {},
-            "test": 1,
-            "nested": {"from_json_file": {}, "test": 1},
-            "array": [{"from_json_file": {}, "test": 1}],
-        }
-        expected_json_string = (
-            '{"test": 1, "nested": {"test": 1}, "array": [{"test": 1}]}'
-        )
+    def test_read_and_write_json(self) -> None:
+        # Act
+        json_string = write_json(JSON_DATA)
+
+        # Assert
+        self.assertEqual(json_string, EXPECTED_JSON_STRING)
 
         # Act
-        json_string = write_json(json_data)
+        read_json_data = read_json(json_string, DataModel)
 
         # Assert
-        self.assertEqual(json_string, expected_json_string)
+        self.assertEqual(read_json_data, JSON_DATA)
 
-    def test_write_json_object(self) -> None:
-        # Arrange
-        json_data = {
-            "from_json_file": {},
-            "test": 1,
-            "nested": {"from_json_file": {}, "test": 1},
-            "array": [{"from_json_file": {}, "test": 1}],
-        }
-        expected_json_object = {
-            "test": 1,
-            "nested": {"test": 1},
-            "array": [{"test": 1}],
-        }
+    def test_read_and_write_json_object(self) -> None:
+        # Act
+        json_object = write_json_object(JSON_DATA)
+
+        # Assert
+        self.assertEqual(json_object, EXPECTED_JSON_OBJECT)
 
         # Act
-        json_object = write_json_object(json_data)
+        read_json_data = read_json_object(json_object, DataModel)
 
         # Assert
-        self.assertEqual(json_object, expected_json_object)
+        self.assertEqual(read_json_data, JSON_DATA)
 
-    def test_write_json_file(self) -> None:
+    def test_read_and_write_json_file(self) -> None:
         # Arrange
-        json_data = {
-            "from_json_file": {},
-            "test": 1,
-            "nested": {"from_json_file": {}, "test": 1},
-            "array": [{"from_json_file": {}, "test": 1}],
-        }
-        expected_json_data = (
-            '{"test": 1, "nested": {"test": 1}, "array": [{"test": 1}]}'
-        )
         file_name = "json_write_test"
         json_file = file_name + ".json"
 
         # Act
-        write_json_file(json_data, file_name)
+        write_json_file(JSON_DATA, file_name)
 
         # Assert
         with open(json_file) as reader:
-            self.assertEqual(reader.read(), expected_json_data)
+            self.assertEqual(reader.read(), EXPECTED_JSON_STRING)
+
+        # Act
+        read_json_data = read_json_file(DataModel, file_name)
+
+        # Assert
+        self.assertEqual(read_json_data, JSON_DATA)
 
         # Cleanup
         remove(json_file)