# Copyright (c) Meta Platforms, Inc. and affiliates. # All rights reserved. # # This source code is licensed under the terms described in the LICENSE file in # the root directory of this source tree. """ Type-safe data interchange for Python data classes. :see: https://github.com/hunyadi/strong_typing """ import abc import base64 import dataclasses import datetime import enum import inspect import ipaddress import sys import typing import uuid from types import ModuleType from typing import ( Any, Callable, Dict, Generic, List, Literal, NamedTuple, Optional, Set, Tuple, Type, TypeVar, Union, ) from .core import JsonType from .exception import JsonKeyError, JsonTypeError, JsonValueError from .inspection import ( TypeLike, create_object, enum_value_types, evaluate_type, get_class_properties, get_class_property, get_resolved_hints, is_dataclass_instance, is_dataclass_type, is_named_tuple_type, is_type_annotated, is_type_literal, is_type_optional, unwrap_annotated_type, unwrap_literal_values, unwrap_optional_type, ) from .mapping import python_field_to_json_property from .name import python_type_to_str E = TypeVar("E", bound=enum.Enum) T = TypeVar("T") R = TypeVar("R") K = TypeVar("K") V = TypeVar("V") class Deserializer(abc.ABC, Generic[T]): "Parses a JSON value into a Python type." def build(self, context: Optional[ModuleType]) -> None: """ Creates auxiliary parsers that this parser is depending on. :param context: A module context for evaluating types specified as a string. """ @abc.abstractmethod def parse(self, data: JsonType) -> T: """ Parses a JSON value into a Python type. :param data: The JSON value to de-serialize. :returns: The Python object that the JSON value de-serializes to. """ class NoneDeserializer(Deserializer[None]): "Parses JSON `null` values into Python `None`." def parse(self, data: JsonType) -> None: if data is not None: raise JsonTypeError(f"`None` type expects JSON `null` but instead received: {data}") return None class BoolDeserializer(Deserializer[bool]): "Parses JSON `boolean` values into Python `bool` type." def parse(self, data: JsonType) -> bool: if not isinstance(data, bool): raise JsonTypeError(f"`bool` type expects JSON `boolean` data but instead received: {data}") return bool(data) class IntDeserializer(Deserializer[int]): "Parses JSON `number` values into Python `int` type." def parse(self, data: JsonType) -> int: if not isinstance(data, int): raise JsonTypeError(f"`int` type expects integer data as JSON `number` but instead received: {data}") return int(data) class FloatDeserializer(Deserializer[float]): "Parses JSON `number` values into Python `float` type." def parse(self, data: JsonType) -> float: if not isinstance(data, float) and not isinstance(data, int): raise JsonTypeError(f"`int` type expects data as JSON `number` but instead received: {data}") return float(data) class StringDeserializer(Deserializer[str]): "Parses JSON `string` values into Python `str` type." def parse(self, data: JsonType) -> str: if not isinstance(data, str): raise JsonTypeError(f"`str` type expects JSON `string` data but instead received: {data}") return str(data) class BytesDeserializer(Deserializer[bytes]): "Parses JSON `string` values of Base64-encoded strings into Python `bytes` type." def parse(self, data: JsonType) -> bytes: if not isinstance(data, str): raise JsonTypeError(f"`bytes` type expects JSON `string` data but instead received: {data}") return base64.b64decode(data, validate=True) class DateTimeDeserializer(Deserializer[datetime.datetime]): "Parses JSON `string` values representing timestamps in ISO 8601 format to Python `datetime` with time zone." def parse(self, data: JsonType) -> datetime.datetime: if not isinstance(data, str): raise JsonTypeError(f"`datetime` type expects JSON `string` data but instead received: {data}") if data.endswith("Z"): data = f"{data[:-1]}+00:00" # Python's isoformat() does not support military time zones like "Zulu" for UTC timestamp = datetime.datetime.fromisoformat(data) if timestamp.tzinfo is None: raise JsonValueError(f"timestamp lacks explicit time zone designator: {data}") return timestamp class DateDeserializer(Deserializer[datetime.date]): "Parses JSON `string` values representing dates in ISO 8601 format to Python `date` type." def parse(self, data: JsonType) -> datetime.date: if not isinstance(data, str): raise JsonTypeError(f"`date` type expects JSON `string` data but instead received: {data}") return datetime.date.fromisoformat(data) class TimeDeserializer(Deserializer[datetime.time]): "Parses JSON `string` values representing time instances in ISO 8601 format to Python `time` type with time zone." def parse(self, data: JsonType) -> datetime.time: if not isinstance(data, str): raise JsonTypeError(f"`time` type expects JSON `string` data but instead received: {data}") return datetime.time.fromisoformat(data) class UUIDDeserializer(Deserializer[uuid.UUID]): "Parses JSON `string` values of UUID strings into Python `uuid.UUID` type." def parse(self, data: JsonType) -> uuid.UUID: if not isinstance(data, str): raise JsonTypeError(f"`UUID` type expects JSON `string` data but instead received: {data}") return uuid.UUID(data) class IPv4Deserializer(Deserializer[ipaddress.IPv4Address]): "Parses JSON `string` values of IPv4 address strings into Python `ipaddress.IPv4Address` type." def parse(self, data: JsonType) -> ipaddress.IPv4Address: if not isinstance(data, str): raise JsonTypeError(f"`IPv4Address` type expects JSON `string` data but instead received: {data}") return ipaddress.IPv4Address(data) class IPv6Deserializer(Deserializer[ipaddress.IPv6Address]): "Parses JSON `string` values of IPv6 address strings into Python `ipaddress.IPv6Address` type." def parse(self, data: JsonType) -> ipaddress.IPv6Address: if not isinstance(data, str): raise JsonTypeError(f"`IPv6Address` type expects JSON `string` data but instead received: {data}") return ipaddress.IPv6Address(data) class ListDeserializer(Deserializer[List[T]]): "Recursively de-serializes a JSON array into a Python `list`." item_type: Type[T] item_parser: Deserializer def __init__(self, item_type: Type[T]) -> None: self.item_type = item_type def build(self, context: Optional[ModuleType]) -> None: self.item_parser = _get_deserializer(self.item_type, context) def parse(self, data: JsonType) -> List[T]: if not isinstance(data, list): type_name = python_type_to_str(self.item_type) raise JsonTypeError(f"type `List[{type_name}]` expects JSON `array` data but instead received: {data}") return [self.item_parser.parse(item) for item in data] class DictDeserializer(Deserializer[Dict[K, V]]): "Recursively de-serializes a JSON object into a Python `dict`." key_type: Type[K] value_type: Type[V] value_parser: Deserializer[V] def __init__(self, key_type: Type[K], value_type: Type[V]) -> None: self.key_type = key_type self.value_type = value_type self._check_key_type() def build(self, context: Optional[ModuleType]) -> None: self.value_parser = _get_deserializer(self.value_type, context) def _check_key_type(self) -> None: if self.key_type is str: return if issubclass(self.key_type, enum.Enum): value_types = enum_value_types(self.key_type) if len(value_types) != 1: raise JsonTypeError( f"type `{self.container_type}` has invalid key type, " f"enumerations must have a consistent member value type but several types found: {value_types}" ) value_type = value_types.pop() if value_type is not str: f"`type `{self.container_type}` has invalid enumeration key type, expected `enum.Enum` with string values" return raise JsonTypeError( f"`type `{self.container_type}` has invalid key type, expected `str` or `enum.Enum` with string values" ) @property def container_type(self) -> str: key_type_name = python_type_to_str(self.key_type) value_type_name = python_type_to_str(self.value_type) return f"Dict[{key_type_name}, {value_type_name}]" def parse(self, data: JsonType) -> Dict[K, V]: if not isinstance(data, dict): raise JsonTypeError( f"`type `{self.container_type}` expects JSON `object` data but instead received: {data}" ) return dict( (self.key_type(key), self.value_parser.parse(value)) # type: ignore[call-arg] for key, value in data.items() ) class SetDeserializer(Deserializer[Set[T]]): "Recursively de-serializes a JSON list into a Python `set`." member_type: Type[T] member_parser: Deserializer def __init__(self, member_type: Type[T]) -> None: self.member_type = member_type def build(self, context: Optional[ModuleType]) -> None: self.member_parser = _get_deserializer(self.member_type, context) def parse(self, data: JsonType) -> Set[T]: if not isinstance(data, list): type_name = python_type_to_str(self.member_type) raise JsonTypeError(f"type `Set[{type_name}]` expects JSON `array` data but instead received: {data}") return set(self.member_parser.parse(item) for item in data) class TupleDeserializer(Deserializer[Tuple[Any, ...]]): "Recursively de-serializes a JSON list into a Python `tuple`." item_types: Tuple[Type[Any], ...] item_parsers: Tuple[Deserializer[Any], ...] def __init__(self, item_types: Tuple[Type[Any], ...]) -> None: self.item_types = item_types def build(self, context: Optional[ModuleType]) -> None: self.item_parsers = tuple(_get_deserializer(item_type, context) for item_type in self.item_types) @property def container_type(self) -> str: type_names = ", ".join(python_type_to_str(item_type) for item_type in self.item_types) return f"Tuple[{type_names}]" def parse(self, data: JsonType) -> Tuple[Any, ...]: if not isinstance(data, list) or len(data) != len(self.item_parsers): if not isinstance(data, list): raise JsonTypeError( f"type `{self.container_type}` expects JSON `array` data but instead received: {data}" ) else: count = len(self.item_parsers) raise JsonValueError( f"type `{self.container_type}` expects a JSON `array` of length {count} but received length {len(data)}" ) return tuple(item_parser.parse(item) for item_parser, item in zip(self.item_parsers, data, strict=False)) class UnionDeserializer(Deserializer): "De-serializes a JSON value (of any type) into a Python union type." member_types: Tuple[type, ...] member_parsers: Tuple[Deserializer, ...] def __init__(self, member_types: Tuple[type, ...]) -> None: self.member_types = member_types def build(self, context: Optional[ModuleType]) -> None: self.member_parsers = tuple(_get_deserializer(member_type, context) for member_type in self.member_types) def parse(self, data: JsonType) -> Any: for member_parser in self.member_parsers: # iterate over potential types of discriminated union try: return member_parser.parse(data) except (JsonKeyError, JsonTypeError): # indicates a required field is missing from JSON dict -OR- the data cannot be cast to the expected type, # i.e. we don't have the type that we are looking for continue type_names = ", ".join(python_type_to_str(member_type) for member_type in self.member_types) raise JsonKeyError(f"type `Union[{type_names}]` could not be instantiated from: {data}") def get_literal_properties(typ: type) -> Set[str]: "Returns the names of all properties in a class that are of a literal type." return set( property_name for property_name, property_type in get_class_properties(typ) if is_type_literal(property_type) ) def get_discriminating_properties(types: Tuple[type, ...]) -> Set[str]: "Returns a set of properties with literal type that are common across all specified classes." if not types or not all(isinstance(typ, type) for typ in types): return set() props = get_literal_properties(types[0]) for typ in types[1:]: props = props & get_literal_properties(typ) return props class TaggedUnionDeserializer(Deserializer): "De-serializes a JSON value with one or more disambiguating properties into a Python union type." member_types: Tuple[type, ...] disambiguating_properties: Set[str] member_parsers: Dict[Tuple[str, Any], Deserializer] def __init__(self, member_types: Tuple[type, ...]) -> None: self.member_types = member_types self.disambiguating_properties = get_discriminating_properties(member_types) def build(self, context: Optional[ModuleType]) -> None: self.member_parsers = {} for member_type in self.member_types: for property_name in self.disambiguating_properties: literal_type = get_class_property(member_type, property_name) if not literal_type: continue for literal_value in unwrap_literal_values(literal_type): tpl = (property_name, literal_value) if tpl in self.member_parsers: raise JsonTypeError( f"disambiguating property `{property_name}` in type `{self.union_type}` has a duplicate value: {literal_value}" ) self.member_parsers[tpl] = _get_deserializer(member_type, context) @property def union_type(self) -> str: type_names = ", ".join(python_type_to_str(member_type) for member_type in self.member_types) return f"Union[{type_names}]" def parse(self, data: JsonType) -> Any: if not isinstance(data, dict): raise JsonTypeError( f"tagged union type `{self.union_type}` expects JSON `object` data but instead received: {data}" ) for property_name in self.disambiguating_properties: disambiguating_value = data.get(property_name) if disambiguating_value is None: continue member_parser = self.member_parsers.get((property_name, disambiguating_value)) if member_parser is None: raise JsonTypeError( f"disambiguating property value is invalid for tagged union type `{self.union_type}`: {data}" ) return member_parser.parse(data) raise JsonTypeError( f"disambiguating property value is missing for tagged union type `{self.union_type}`: {data}" ) class LiteralDeserializer(Deserializer): "De-serializes a JSON value into a Python literal type." values: Tuple[Any, ...] parser: Deserializer def __init__(self, values: Tuple[Any, ...]) -> None: self.values = values def build(self, context: Optional[ModuleType]) -> None: literal_type_tuple = tuple(type(value) for value in self.values) literal_type_set = set(literal_type_tuple) if len(literal_type_set) != 1: value_names = ", ".join(repr(value) for value in self.values) raise TypeError( f"type `Literal[{value_names}]` expects consistent literal value types but got: {literal_type_tuple}" ) literal_type = literal_type_set.pop() self.parser = _get_deserializer(literal_type, context) def parse(self, data: JsonType) -> Any: value = self.parser.parse(data) if value not in self.values: value_names = ", ".join(repr(value) for value in self.values) raise JsonTypeError(f"type `Literal[{value_names}]` could not be instantiated from: {data}") return value class EnumDeserializer(Deserializer[E]): "Returns an enumeration instance based on the enumeration value read from a JSON value." enum_type: Type[E] def __init__(self, enum_type: Type[E]) -> None: self.enum_type = enum_type def parse(self, data: JsonType) -> E: return self.enum_type(data) class CustomDeserializer(Deserializer[T]): "Uses the `from_json` class method in class to de-serialize the object from JSON." converter: Callable[[JsonType], T] def __init__(self, converter: Callable[[JsonType], T]) -> None: self.converter = converter def parse(self, data: JsonType) -> T: return self.converter(data) class FieldDeserializer(abc.ABC, Generic[T, R]): """ Deserializes a JSON property into a Python object field. :param property_name: The name of the JSON property to read from a JSON `object`. :param field_name: The name of the field in a Python class to write data to. :param parser: A compatible deserializer that can handle the field's type. """ property_name: str field_name: str parser: Deserializer[T] def __init__(self, property_name: str, field_name: str, parser: Deserializer[T]) -> None: self.property_name = property_name self.field_name = field_name self.parser = parser @abc.abstractmethod def parse_field(self, data: Dict[str, JsonType]) -> R: ... class RequiredFieldDeserializer(FieldDeserializer[T, T]): "Deserializes a JSON property into a mandatory Python object field." def parse_field(self, data: Dict[str, JsonType]) -> T: if self.property_name not in data: raise JsonKeyError(f"missing required property `{self.property_name}` from JSON object: {data}") return self.parser.parse(data[self.property_name]) class OptionalFieldDeserializer(FieldDeserializer[T, Optional[T]]): "Deserializes a JSON property into an optional Python object field with a default value of `None`." def parse_field(self, data: Dict[str, JsonType]) -> Optional[T]: value = data.get(self.property_name) if value is not None: return self.parser.parse(value) else: return None class DefaultFieldDeserializer(FieldDeserializer[T, T]): "Deserializes a JSON property into a Python object field with an explicit default value." default_value: T def __init__( self, property_name: str, field_name: str, parser: Deserializer, default_value: T, ) -> None: super().__init__(property_name, field_name, parser) self.default_value = default_value def parse_field(self, data: Dict[str, JsonType]) -> T: value = data.get(self.property_name) if value is not None: return self.parser.parse(value) else: return self.default_value class DefaultFactoryFieldDeserializer(FieldDeserializer[T, T]): "Deserializes a JSON property into an optional Python object field with an explicit default value factory." default_factory: Callable[[], T] def __init__( self, property_name: str, field_name: str, parser: Deserializer[T], default_factory: Callable[[], T], ) -> None: super().__init__(property_name, field_name, parser) self.default_factory = default_factory def parse_field(self, data: Dict[str, JsonType]) -> T: value = data.get(self.property_name) if value is not None: return self.parser.parse(value) else: return self.default_factory() class ClassDeserializer(Deserializer[T]): "Base class for de-serializing class-like types such as data classes, named tuples and regular classes." class_type: type property_parsers: List[FieldDeserializer] property_fields: Set[str] def __init__(self, class_type: Type[T]) -> None: self.class_type = class_type def assign(self, property_parsers: List[FieldDeserializer]) -> None: self.property_parsers = property_parsers self.property_fields = set(property_parser.property_name for property_parser in property_parsers) def parse(self, data: JsonType) -> T: if not isinstance(data, dict): type_name = python_type_to_str(self.class_type) raise JsonTypeError(f"`type `{type_name}` expects JSON `object` data but instead received: {data}") object_data: Dict[str, JsonType] = typing.cast(Dict[str, JsonType], data) field_values = {} for property_parser in self.property_parsers: field_values[property_parser.field_name] = property_parser.parse_field(object_data) if not self.property_fields.issuperset(object_data): unassigned_names = [name for name in object_data if name not in self.property_fields] raise JsonKeyError(f"unrecognized fields in JSON object: {unassigned_names}") return self.create(**field_values) def create(self, **field_values: Any) -> T: "Instantiates an object with a collection of property values." obj: T = create_object(self.class_type) # use `setattr` on newly created object instance for field_name, field_value in field_values.items(): setattr(obj, field_name, field_value) return obj class NamedTupleDeserializer(ClassDeserializer[NamedTuple]): "De-serializes a named tuple from a JSON `object`." def build(self, context: Optional[ModuleType]) -> None: property_parsers: List[FieldDeserializer] = [ RequiredFieldDeserializer(field_name, field_name, _get_deserializer(field_type, context)) for field_name, field_type in get_resolved_hints(self.class_type).items() ] super().assign(property_parsers) def create(self, **field_values: Any) -> NamedTuple: return self.class_type(**field_values) class DataclassDeserializer(ClassDeserializer[T]): "De-serializes a data class from a JSON `object`." def __init__(self, class_type: Type[T]) -> None: if not dataclasses.is_dataclass(class_type): raise TypeError("expected: data-class type") super().__init__(class_type) # type: ignore[arg-type] def build(self, context: Optional[ModuleType]) -> None: property_parsers: List[FieldDeserializer] = [] resolved_hints = get_resolved_hints(self.class_type) for field in dataclasses.fields(self.class_type): field_type = resolved_hints[field.name] property_name = python_field_to_json_property(field.name, field_type) is_optional = is_type_optional(field_type) has_default = field.default is not dataclasses.MISSING has_default_factory = field.default_factory is not dataclasses.MISSING if is_optional: required_type: Type[T] = unwrap_optional_type(field_type) else: required_type = field_type parser = _get_deserializer(required_type, context) if has_default: field_parser: FieldDeserializer = DefaultFieldDeserializer( property_name, field.name, parser, field.default ) elif has_default_factory: default_factory = typing.cast(Callable[[], Any], field.default_factory) field_parser = DefaultFactoryFieldDeserializer(property_name, field.name, parser, default_factory) elif is_optional: field_parser = OptionalFieldDeserializer(property_name, field.name, parser) else: field_parser = RequiredFieldDeserializer(property_name, field.name, parser) property_parsers.append(field_parser) super().assign(property_parsers) class FrozenDataclassDeserializer(DataclassDeserializer[T]): "De-serializes a frozen data class from a JSON `object`." def create(self, **field_values: Any) -> T: "Instantiates an object with a collection of property values." # create object instance without calling `__init__` obj: T = create_object(self.class_type) # can't use `setattr` on frozen dataclasses, pass member variable values to `__init__` obj.__init__(**field_values) # type: ignore return obj class TypedClassDeserializer(ClassDeserializer[T]): "De-serializes a class with type annotations from a JSON `object` by iterating over class properties." def build(self, context: Optional[ModuleType]) -> None: property_parsers: List[FieldDeserializer] = [] for field_name, field_type in get_resolved_hints(self.class_type).items(): property_name = python_field_to_json_property(field_name, field_type) is_optional = is_type_optional(field_type) if is_optional: required_type: Type[T] = unwrap_optional_type(field_type) else: required_type = field_type parser = _get_deserializer(required_type, context) if is_optional: field_parser: FieldDeserializer = OptionalFieldDeserializer(property_name, field_name, parser) else: field_parser = RequiredFieldDeserializer(property_name, field_name, parser) property_parsers.append(field_parser) super().assign(property_parsers) def create_deserializer(typ: TypeLike, context: Optional[ModuleType] = None) -> Deserializer: """ Creates a de-serializer engine to produce a Python object from an object obtained from a JSON string. When de-serializing a JSON object into a Python object, the following transformations are applied: * Fundamental types are parsed as `bool`, `int`, `float` or `str`. * Date and time types are parsed from the ISO 8601 format with time zone into the corresponding Python type `datetime`, `date` or `time`. * Byte arrays are read from a string with Base64 encoding into a `bytes` instance. * UUIDs are extracted from a UUID string compliant with RFC 4122 into a `uuid.UUID` instance. * Enumerations are instantiated with a lookup on enumeration value. * Containers (e.g. `list`, `dict`, `set`, `tuple`) are parsed recursively. * Complex objects with properties (including data class types) are populated from dictionaries of key-value pairs using reflection (enumerating type annotations). :raises TypeError: A de-serializer engine cannot be constructed for the input type. """ if context is None: if isinstance(typ, type): context = sys.modules[typ.__module__] return _get_deserializer(typ, context) _CACHE: Dict[Tuple[str, str], Deserializer] = {} def _get_deserializer(typ: TypeLike, context: Optional[ModuleType]) -> Deserializer: "Creates or re-uses a de-serializer engine to parse an object obtained from a JSON string." cache_key = None if isinstance(typ, (str, typing.ForwardRef)): if context is None: raise TypeError(f"missing context for evaluating type: {typ}") if isinstance(typ, str): if hasattr(context, typ): cache_key = (context.__name__, typ) elif isinstance(typ, typing.ForwardRef): if hasattr(context, typ.__forward_arg__): cache_key = (context.__name__, typ.__forward_arg__) typ = evaluate_type(typ, context) typ = unwrap_annotated_type(typ) if is_type_annotated(typ) else typ if isinstance(typ, type) and typing.get_origin(typ) is None: cache_key = (typ.__module__, typ.__name__) if cache_key is not None: deserializer = _CACHE.get(cache_key) if deserializer is None: deserializer = _create_deserializer(typ) # store de-serializer immediately in cache to avoid stack overflow for recursive types _CACHE[cache_key] = deserializer if isinstance(typ, type): # use type's own module as context for evaluating member types context = sys.modules[typ.__module__] # create any de-serializers this de-serializer is depending on deserializer.build(context) else: # special forms are not always hashable, create a new de-serializer every time deserializer = _create_deserializer(typ) deserializer.build(context) return deserializer def _create_deserializer(typ: TypeLike) -> Deserializer: "Creates a de-serializer engine to parse an object obtained from a JSON string." # check for well-known types if typ is type(None): return NoneDeserializer() elif typ is bool: return BoolDeserializer() elif typ is int: return IntDeserializer() elif typ is float: return FloatDeserializer() elif typ is str: return StringDeserializer() elif typ is bytes: return BytesDeserializer() elif typ is datetime.datetime: return DateTimeDeserializer() elif typ is datetime.date: return DateDeserializer() elif typ is datetime.time: return TimeDeserializer() elif typ is uuid.UUID: return UUIDDeserializer() elif typ is ipaddress.IPv4Address: return IPv4Deserializer() elif typ is ipaddress.IPv6Address: return IPv6Deserializer() # dynamically-typed collection types if typ is list: raise TypeError("explicit item type required: use `List[T]` instead of `list`") if typ is dict: raise TypeError("explicit key and value types required: use `Dict[K, V]` instead of `dict`") if typ is set: raise TypeError("explicit member type required: use `Set[T]` instead of `set`") if typ is tuple: raise TypeError("explicit item type list required: use `Tuple[T, ...]` instead of `tuple`") # generic types (e.g. list, dict, set, etc.) origin_type = typing.get_origin(typ) if origin_type is list: (list_item_type,) = typing.get_args(typ) # unpack single tuple element return ListDeserializer(list_item_type) elif origin_type is dict: key_type, value_type = typing.get_args(typ) return DictDeserializer(key_type, value_type) elif origin_type is set: (set_member_type,) = typing.get_args(typ) # unpack single tuple element return SetDeserializer(set_member_type) elif origin_type is tuple: return TupleDeserializer(typing.get_args(typ)) elif origin_type is Union: union_args = typing.get_args(typ) if get_discriminating_properties(union_args): return TaggedUnionDeserializer(union_args) else: return UnionDeserializer(union_args) elif origin_type is Literal: return LiteralDeserializer(typing.get_args(typ)) if not inspect.isclass(typ): if is_dataclass_instance(typ): raise TypeError(f"dataclass type expected but got instance: {typ}") else: raise TypeError(f"unable to de-serialize unrecognized type: {typ}") if issubclass(typ, enum.Enum): return EnumDeserializer(typ) if is_named_tuple_type(typ): return NamedTupleDeserializer(typ) # check if object has custom serialization method convert_func = getattr(typ, "from_json", None) if callable(convert_func): return CustomDeserializer(convert_func) if is_dataclass_type(typ): dataclass_params = getattr(typ, "__dataclass_params__", None) if dataclass_params is not None and dataclass_params.frozen: return FrozenDataclassDeserializer(typ) else: return DataclassDeserializer(typ) return TypedClassDeserializer(typ)