import typing from datetime import datetime from enum import EnumMeta from typing import Any, Dict, Type from dateutil import parser def from_json(template_type: Any, data: Any) -> Any: """ Converts data from a JSON parse into an instantiation of the Python object specified by template_type. :param template_type: the template type to deserialize into :param data: the data to deserialize to the class """ # Approach for deserialization here: # https://stackoverflow.com/a/40639688/2319844 # If it's a forward reference, evaluate it to figure out the actual # type. This allows for types that have to be put into a string. if isinstance(template_type, typing.ForwardRef): # type: ignore template_type = template_type._evaluate(globals(), locals()) annotations: Dict[str, Type] = getattr(template_type, "__annotations__", {}) # Handle primitive of objects instance: Any = None if data is None: instance = None # Handle generics. List[*], Dict[*, *] in particular. elif type(template_type) == typing._GenericAlias: # type: ignore if getattr(template_type, "__origin__") == typing.Union: template_type = template_type.__args__[0] instance = from_json(template_type, data) else: # Having to use this because things changed in Python 3.7. class_name = template_type._name # This is not very elegant since it doesn't allow things which # sublass from List or Dict. For my purposes, this doesn't matter. if class_name == "List": inner_type = template_type.__args__[0] instance = [from_json(inner_type, value) for value in data] elif class_name == "Dict": key_type, val_type = template_type.__args__ instance = { from_json(key_type, key): from_json(val_type, value) for key, value in data.items() } else: raise Exception( "Trying to deserialize an unsupported type: {}".format( template_type._name ) ) elif template_type == str or issubclass(template_type, str): instance = data elif template_type == int or issubclass(template_type, int): instance = int(data) elif template_type == bool or issubclass(template_type, bool): instance = bool(data) elif type(template_type) == EnumMeta: if type(data) == dict: instance = template_type(data.get("_value_")) else: instance = template_type(data) elif template_type == datetime: if type(data) == int: instance = datetime.fromtimestamp(data / 1000) else: instance = parser.parse(data) # Handle everything else by first instantiating the class, then adding # all of the sub-elements, recursively calling from_json on them. else: # for field, field_type in annotations.items(): # value = data.get(field) # setattr(instance, field, from_json(field_type, value)) instance = template_type( **{ field: from_json(field_type, data.get(field)) for field, field_type in annotations.items() } ) return instance