Module typing_json.encoding
The typing_json.encoding provides functionality for type-aware JSON-encoding of objects.
The core functionality is provided by to_json_obj(), which JSON-encodes instances
of basic JSON types, typed collections from the typing module, literal types, union types, optional types
and (certain) typed namedtuples.
The JSON-encoding preserves all information necessary to reconstruct the at decoding time (cf. from_json_obj()).
(Version: 0.1.2)
Expand source code
#pylint:disable = line-too-long, invalid-name
"""
The `typing_json.encoding` provides functionality for type-aware JSON-encoding of objects.
The core functionality is provided by `typing_json.encoding.to_json_obj`, which JSON-encodes instances
of basic JSON types, typed collections from the `typing` module, literal types, union types, optional types
and (certain) typed namedtuples.
The JSON-encoding preserves all information necessary to reconstruct the at decoding time (cf. `typing_json.decoding.from_json_obj`).
(Version: 0.1.2)
"""
# standard imports
from collections import deque, OrderedDict
from collections.abc import Mapping
from decimal import Decimal
from enum import EnumMeta
import json
from typing import Any, Callable, List, Optional, Union, Type
# external dependencies
from typing_extensions import Literal
# internal imports
from typing_json.typechecking import is_instance, is_keyable, is_namedtuple, is_typecheckable, is_typed_dict, JSON_BASE_TYPES, short_str
_UNREACHABLE_ERROR_MSG = "Should never reach this point, please open an issue on GitHub."
def _not_json_encodable(message: str, failure_callback: Optional[Callable[[str], None]]) -> Literal[False]:
""" Utility message to fail (return `False`) by first calling an optional failure callback. """
if failure_callback:
failure_callback(message)
return False
def is_json_encodable(t: Type, failure_callback: Optional[Callable[[str], None]] = None) -> bool:
"""
Checks whether a type `t` can be encoded into JSON (or decoded from JSON) using the `typing_json` library.
The optional parameter `failure_callback` can be used to collect a detailed trace of
the reasons behind this method returning `False` on a given type `t`.
Currently, a type `t` is JSON encodable according to this method if it is typecheckable according to
`typing_json.typechecking.is_typecheckable` and it satisfies one of the following conditions:
- if `t` is one of the JSON basic types `bool`, `int`, `float`, `str`, `NoneType`;
- if `t` is a `decimal.Decimal`;
- if `t` is `None` (used as an alias for `NoneType`);
- if `t` is an enum (i.e. `isinstance(t, EnumMeta)`);
- if `t` is a namedtuple according to `typing_json.typechecking.is_namedtuple` and all its fields are JSON encodable;
- if `t` is a typed dictionary according to `typing_json.typechecking.is_typed_dict` and all its values are JSON encodable;
- if `t` is one of `typing.List`, `typing.Set`, `typing.FrozenSet`, `typing.Deque`, `typing.Optional` or a variadic `typing.Tuple` and its generic type argument is JSON encodable;
- if `t` is a `typing.Union` or a fixed-length `typing.Tuple` and all of its generic type arguments are JSON encodable;
- if `t` is a `typing.Dict`, `typing.OrderedDict` or `typing.Mapping`, its generic key type is keyable (according to `typing_json.typechecking.is_keyable`) and both its generic key and value types are JSON encodable;
- if `t` is a `typing_extensions.Literal` and all of its literal arguments are of JSON basic type.
(Version 0.1.3)
"""
# pylint: disable = too-many-return-statements, too-many-branches
if not is_typecheckable(t, failure_callback=failure_callback):
# only typecheckable types are encodable
return _not_json_encodable("Type %s is not typecheckable."%str(t), failure_callback=failure_callback)
if t in JSON_BASE_TYPES:
# JSON basic types are encodable
return True
if t is Decimal:
# `decimal.Decimal` is encodable
return True
if t is None:
# `None` canbe used as an alias for class `NoneType`
return True
if isinstance(t, EnumMeta):
# enums are encodable
return True
if is_namedtuple(t):
field_types = getattr(t, "_field_types")
if all(is_json_encodable(field_types[field], failure_callback=failure_callback) for field in field_types):
# namedtuples are encodable if all their fields are of encodable types
return True
return _not_json_encodable("Not all fields of namedtuple %s are json-encodable."%str(t), failure_callback=failure_callback)
if is_typed_dict(t):
field_types = getattr(t, "__annotations__")
if all(is_json_encodable(field_types[field], failure_callback=failure_callback) for field in field_types):
# typed dicts are encodable if all their fields are of encodable types
return True
return _not_json_encodable("Not all fields of typed dict %s are json-encodable."%str(t), failure_callback=failure_callback)
if hasattr(t, "__origin__") and hasattr(t, "__args__"):
# `typing` generics
if t.__origin__ in (list, set, frozenset, deque, Optional):
if is_json_encodable(t.__args__[0], failure_callback=failure_callback):
# `typing.List`, `typing.Set`, `typing.FrozenSet`, `typing.Deque` and `typing.Optional` are encodable if their generic type argument is encodable
return True
return _not_json_encodable("Type of elements in %s is not json-encodable."%str(t), failure_callback=failure_callback)
if t.__origin__ is tuple:
# `typing.Tuple`
if len(t.__args__) == 2 and t.__args__[1] is ...: # pylint:disable=no-else-return
if is_json_encodable(t.__args__[0], failure_callback=failure_callback):
# variadic `typing.Tuple` are encodable if their generic type argument is encodable
return True
return _not_json_encodable("Type of elements in %s is not json-encodable."%str(t), failure_callback=failure_callback)
else:
if all(is_json_encodable(s, failure_callback=failure_callback) for s in t.__args__):
# fixed-length `typing.Tuple` are encodable if all their generic type arguments are encodable
return True
return _not_json_encodable("Type of some element in %s is not json-encodable."%str(t), failure_callback=failure_callback)
if t.__origin__ is Union:
if all(is_json_encodable(s, failure_callback=failure_callback) for s in t.__args__):
# `typing.Union` are encodable if all their generic type arguments are encodable
return True
return _not_json_encodable("Some type in %s is not json-encodable."%str(t), failure_callback=failure_callback)
if t.__origin__ in (dict, OrderedDict, Mapping):
# `typing.Dict`, `typing.OrderedDict` and `typing.Mapping` are encodable if their generic key and value types are encodable and their key type is keyable
if not is_keyable(t.__args__[0], failure_callback=failure_callback):
return _not_json_encodable("Type of keys in %s is not keyable."%str(t), failure_callback=failure_callback)
if not is_json_encodable(t.__args__[0], failure_callback=failure_callback):
return _not_json_encodable("Type of keys in %s is not json-encodable."%str(t), failure_callback=failure_callback)
if not is_json_encodable(t.__args__[1], failure_callback=failure_callback):
return _not_json_encodable("Type of values in %s is not json-encodable."%str(t), failure_callback=failure_callback)
return True
if t.__origin__ is Literal:
# `typing_extensions.Literal` are encodable as long as their literals are JSON basic types, which is always the case if they are typecheckable.
return True
return False
def _to_json_obj_namedtuple(obj, field_types, use_decimal=True, namedtuples_as_lists=False):
# pylint:disable=invalid-name
if namedtuples_as_lists:
return [to_json_obj(getattr(obj, field), field_type, use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for field, field_type in field_types.items()]
json_dict = OrderedDict() # type:ignore
for field, field_type in field_types.items():
json_dict[field] = to_json_obj(getattr(obj, field), field_type, use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists)
return json_dict
def _to_json_obj_homogeneous_collection(obj, element_t, use_decimal=True, namedtuples_as_lists=False):
# pylint:disable=invalid-name,too-many-return-statements
if element_t in JSON_BASE_TYPES or element_t in (None, type(None)):
return list(obj)
if element_t is Decimal:
if use_decimal:
return list(obj)
return [str(el) for el in obj]
if isinstance(element_t, EnumMeta):
return [el._name_ for el in obj] # pylint:disable=protected-access
if is_namedtuple(element_t):
field_types = getattr(element_t, "_field_types")
return [_to_json_obj_namedtuple(el, field_types, use_decimal=use_decimal, namedtuples_as_lists=namedtuples_as_lists) for el in obj]
return [to_json_obj(x, element_t, use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for x in obj]
def to_json_obj(obj: Any, t: Type, use_decimal: bool = False, typecheck: bool = True, namedtuples_as_lists=False) -> Any:
"""
Encodes an instance `obj` of typecheckable type `t` into a JSON object.
The optional `use_decimal` parameter can be used to specify that instances of
`decimal.Decimal` can be used in the output: if `False`, they are converted to strings.
This method raises `TypeError` if type `t` is not typecheckable according to `typing_json.typechecking.is_typecheckable`.
This method raises `TypeError` if `obj` is not of type `t` according to `typing_json.typechecking.is_instance`.
Currently, this method acts as follows on an instance `obj` of type `t`:
- if `t` is one of the JSON basic types `bool`, `int`, `float`, `str`, `NoneType`, the instance `obj` is returned unchanged;
- if `t` is `decimal.Decimal` and `use_decimal` is `False` (default), `str(obj)` is returned;
- if `t` is `decimal.Decimal` and `use_decimal` is `True`, `obj` is returned unchanged;
- if `t` is `None` (used as an alias for `NoneType`), `None` is returned;
- if `t` is an enum (i.e. `isinstance(t, EnumMeta)`), the enum value name `obj._name_` is returned;
- if `t` is a namedtuple according to `typing_json.typechecking.is_namedtuple` and all its fields are JSON encodable and `namedtuples_as_lists` is `False`, this method is called recursively on all field values and then an ordered dictionary is returned with the field names as names and the JSON-encoded field values as corresponding values;
- if `t` is a namedtuple according to `typing_json.typechecking.is_namedtuple` and all its fields are JSON encodable and `namedtuples_as_lists` is `True`, this method is called recursively on all field values and then a list is returned with the JSON-encoded field values appearing in the same order as the namedtuple fields (which are not explicitly encoded);
- if `t` is a typed dict according to `typing_json.typechecking.is_typed_dict` and all its values are JSON encodable, then a dictionary is returned with the same keys as `obj` and JSON-encoded values using the types specified by `t`.
- if `t` is `typing.Union`, the generic type arguments in the union are tried one after the other until a `u` is found such that `is_instance(obj, u)`, then `obj` is JSON-encoded using `u` as its type.
- if `t` is a `typing_extensions.Literal`, `obj` is returned unchanged;
- if `t` is one of `typing.List`, `typing.Set`, `typing.FrozenSet`, `typing.Deque` or `typing.Tuple`, a list is returned containing the elements of the original collection, recursively JSON-encoded;
- if `t` is a `typing.Dict` or `typing.Mapping`, a dictionary (`dict`) is returned with JSON-encoded values from the original dictionary/mapping, associated to either then JSON-encoded keys or a stringified version of the JSON-encoded keys (cf. below);
- if `t` is `typing.OrderedDict`, an ordered dictionary (`collections.OrderedDict`) is returned with JSON-encoded values from the original dictionary/mapping, associated to either then JSON-encoded keys or a stringified version of the JSON-encoded keys (cf. below).
In the case of dictionaries, it is not necessarily the case keys will be compatible with the JSON specification in their JSON-encoded form.
When encoding dictionaries, the keys used in the encoding follow the following criteria:
- if the key type is a JOSN basic type, `decimal.Decimal` or an enumeration type, the JSON encoding of the keys is used;
- otherwise, the stringified version of the JSON encoding (using `json.dumps`) is used;
Literals can only be of JSON basic type.
An optional parameter `typecheck` (default: `True`) can be used to skip the check that `t` be JSON encodable and that `obj` be an instance of `t`.
The parameter `typecheck` is set to `False` in all recursive calls (i.e. typechecking is only done once).
(Version 0.1.3)
"""
# pylint:disable=invalid-name,too-many-return-statements,too-many-branches
if typecheck:
trace: List[str] = []
def failure_callback(message: str) -> None:
trace.append(message)
if not is_json_encodable(t, failure_callback=failure_callback):
# Argument `t` must be JSON encodable.
raise TypeError("Type %s is not json-encodable. Trace:\n%s"%(str(t), "\n".join(trace)))
trace = []
if not is_instance(obj, t, failure_callback=failure_callback):
# Argument `obj` must be an instance of argument `t`.
raise TypeError("Object %s is not of type %s. Trace:\n%s"%(short_str(obj), str(t), "\n".join(trace)))
if t in JSON_BASE_TYPES:
# JSON basic types are returned unchanged.
return obj
if t is Decimal:
# If `use_decimal` is `True`, `obj` is returned unchanged:
if use_decimal:
return obj
# If `use_decimal` is `False` (default), instances of `decimal.Decimal` are encoded as strings.
return str(obj)
if t in (None, type(None)):
# `None` can be used as an alias for `NoneType`.
return None
if isinstance(t, EnumMeta):
# Enum values are encoded by their name.
return obj._name_ # pylint:disable=protected-access
if is_namedtuple(t):
# Namedtuples are encoded as ordered dictionaries, with their fields as keys and the JSON-encoded field values as corresponding values.
field_types = getattr(t, "_field_types")
return _to_json_obj_namedtuple(obj, field_types, use_decimal=use_decimal, namedtuples_as_lists=namedtuples_as_lists)
if is_typed_dict(t):
# Typed dicts are encoded as ordered dictionaries, with their fields as keys and the JSON-encoded field values as corresponding values.
field_types = getattr(t, "__annotations__")
# return _to_json_obj_namedtuple(obj, field_types, use_decimal=use_decimal, namedtuples_as_lists=namedtuples_as_lists)
# A `dict`is used for `typing.Dict` and `typing.Mapping`.
return {
field: to_json_obj(obj[field], field_type,
use_decimal=use_decimal,
typecheck=False,
namedtuples_as_lists=namedtuples_as_lists)
for field, field_type in field_types.items()
}
if hasattr(t, "__origin__") and hasattr(t, "__args__"):
# Generics from the `typing` module.
if t.__origin__ is Union:
# values in a `typing.Union` are JSON-encoded using the first type in the union that the object is found to be an instance of.
for s in t.__args__:
if is_instance(obj, s):
return to_json_obj(obj, s, use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists)
raise AssertionError(_UNREACHABLE_ERROR_MSG) # pragma: no cover
if t.__origin__ is Literal:
# `typing_extensions.Literal` are returned unchanged
return obj
if t.__origin__ in (list, set, frozenset, deque):
# `typing.List`, `typing.Set`, `typing.FrozenSet` and `typing.Deque` are turned into lists, with their elements recursively JSON-encoded
return _to_json_obj_homogeneous_collection(obj, t.__args__[0], use_decimal=use_decimal, namedtuples_as_lists=namedtuples_as_lists)
if t.__origin__ is tuple:
# `typing.Tuple` are turned into lists, with their elements recursively JSON-encoded
if len(t.__args__) == 2 and t.__args__[1] is ...: # pylint:disable=no-else-return
return _to_json_obj_homogeneous_collection(obj, t.__args__[0], use_decimal=use_decimal, namedtuples_as_lists=namedtuples_as_lists)
else:
return [to_json_obj(x, t.__args__[i], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for i, x in enumerate(obj)]
if t.__origin__ in (dict, OrderedDict, Mapping):
# `typing.Dict` and `typing.Mapping` are turned into dictionaries and `typing.OrderedDict` are turned into ordered dictionaries.
# The values are recursively JSON-encoded. Keys require special handling.
fields = [field for field in obj] # pylint: disable = unnecessary-comprehension
if t.__args__[0] in JSON_BASE_TYPES+(Decimal, None,):
# Keys of JSON basic types, `decimal.Decimal` and `None` are recursively JSON-encoded.
# encoded_fields = [field for field in fields] # pylint: disable = unnecessary-comprehension
encoded_fields = [to_json_obj(field, t.__args__[0], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for field in fields]
elif (hasattr(t.__args__[0], "__origin__") and t.__args__[0].__origin__ is Literal):
# Keys of `typing_extensions.Literal` types are recursively JSON-encoded.
# encoded_fields = [field for field in fields] # pylint: disable = unnecessary-comprehension
encoded_fields = [to_json_obj(field, t.__args__[0], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for field in fields]
elif isinstance(t.__args__[0], EnumMeta):
# Keys of enumeration types are recursively JSON-encoded.
encoded_fields = [to_json_obj(field, t.__args__[0], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for field in fields]
else:
# Keys of any other type are recursively JSON-encoded and then JSON dumped to strings.
encoded_fields = [json.dumps(to_json_obj(field, t.__args__[0], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists)) for field in fields]
if t.__origin__ in (dict, Mapping):
# A `dict`is used for `typing.Dict` and `typing.Mapping`.
return {encoded_fields[i]: to_json_obj(obj[field], t.__args__[1], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for i, field in enumerate(fields)}
if t.__origin__ is OrderedDict:
# A `collections.OrderedDict` is used for `typing.OrderedDict`.
new_ordered_dict = OrderedDict() # type:ignore
for i, field in enumerate(fields):
new_ordered_dict[encoded_fields[i]] = to_json_obj(obj[field], t.__args__[1], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists)
return new_ordered_dict
raise AssertionError(_UNREACHABLE_ERROR_MSG) # pragma: no cover
Functions
def is_json_encodable(t: Type, failure_callback: Union[Callable[[str], NoneType], NoneType] = None) ‑> bool-
Checks whether a type
tcan be encoded into JSON (or decoded from JSON) using thetyping_jsonlibrary.The optional parameter
failure_callbackcan be used to collect a detailed trace of the reasons behind this method returningFalseon a given typet.Currently, a type
tis JSON encodable according to this method if it is typecheckable according tois_typecheckable()and it satisfies one of the following conditions:- if
tis one of the JSON basic typesbool,int,float,str,NoneType; - if
tis adecimal.Decimal; - if
tisNone(used as an alias forNoneType); - if
tis an enum (i.e.isinstance(t, EnumMeta)); - if
tis a namedtuple according tois_namedtuple()and all its fields are JSON encodable; - if
tis a typed dictionary according tois_typed_dict()and all its values are JSON encodable; - if
tis one oftyping.List,typing.Set,typing.FrozenSet,typing.Deque,typing.Optionalor a variadictyping.Tupleand its generic type argument is JSON encodable; - if
tis atyping.Unionor a fixed-lengthtyping.Tupleand all of its generic type arguments are JSON encodable; - if
tis atyping.Dict,typing.OrderedDictortyping.Mapping, its generic key type is keyable (according tois_keyable()) and both its generic key and value types are JSON encodable; - if
tis atyping_extensions.Literaland all of its literal arguments are of JSON basic type.
(Version 0.1.3)
Expand source code
def is_json_encodable(t: Type, failure_callback: Optional[Callable[[str], None]] = None) -> bool: """ Checks whether a type `t` can be encoded into JSON (or decoded from JSON) using the `typing_json` library. The optional parameter `failure_callback` can be used to collect a detailed trace of the reasons behind this method returning `False` on a given type `t`. Currently, a type `t` is JSON encodable according to this method if it is typecheckable according to `typing_json.typechecking.is_typecheckable` and it satisfies one of the following conditions: - if `t` is one of the JSON basic types `bool`, `int`, `float`, `str`, `NoneType`; - if `t` is a `decimal.Decimal`; - if `t` is `None` (used as an alias for `NoneType`); - if `t` is an enum (i.e. `isinstance(t, EnumMeta)`); - if `t` is a namedtuple according to `typing_json.typechecking.is_namedtuple` and all its fields are JSON encodable; - if `t` is a typed dictionary according to `typing_json.typechecking.is_typed_dict` and all its values are JSON encodable; - if `t` is one of `typing.List`, `typing.Set`, `typing.FrozenSet`, `typing.Deque`, `typing.Optional` or a variadic `typing.Tuple` and its generic type argument is JSON encodable; - if `t` is a `typing.Union` or a fixed-length `typing.Tuple` and all of its generic type arguments are JSON encodable; - if `t` is a `typing.Dict`, `typing.OrderedDict` or `typing.Mapping`, its generic key type is keyable (according to `typing_json.typechecking.is_keyable`) and both its generic key and value types are JSON encodable; - if `t` is a `typing_extensions.Literal` and all of its literal arguments are of JSON basic type. (Version 0.1.3) """ # pylint: disable = too-many-return-statements, too-many-branches if not is_typecheckable(t, failure_callback=failure_callback): # only typecheckable types are encodable return _not_json_encodable("Type %s is not typecheckable."%str(t), failure_callback=failure_callback) if t in JSON_BASE_TYPES: # JSON basic types are encodable return True if t is Decimal: # `decimal.Decimal` is encodable return True if t is None: # `None` canbe used as an alias for class `NoneType` return True if isinstance(t, EnumMeta): # enums are encodable return True if is_namedtuple(t): field_types = getattr(t, "_field_types") if all(is_json_encodable(field_types[field], failure_callback=failure_callback) for field in field_types): # namedtuples are encodable if all their fields are of encodable types return True return _not_json_encodable("Not all fields of namedtuple %s are json-encodable."%str(t), failure_callback=failure_callback) if is_typed_dict(t): field_types = getattr(t, "__annotations__") if all(is_json_encodable(field_types[field], failure_callback=failure_callback) for field in field_types): # typed dicts are encodable if all their fields are of encodable types return True return _not_json_encodable("Not all fields of typed dict %s are json-encodable."%str(t), failure_callback=failure_callback) if hasattr(t, "__origin__") and hasattr(t, "__args__"): # `typing` generics if t.__origin__ in (list, set, frozenset, deque, Optional): if is_json_encodable(t.__args__[0], failure_callback=failure_callback): # `typing.List`, `typing.Set`, `typing.FrozenSet`, `typing.Deque` and `typing.Optional` are encodable if their generic type argument is encodable return True return _not_json_encodable("Type of elements in %s is not json-encodable."%str(t), failure_callback=failure_callback) if t.__origin__ is tuple: # `typing.Tuple` if len(t.__args__) == 2 and t.__args__[1] is ...: # pylint:disable=no-else-return if is_json_encodable(t.__args__[0], failure_callback=failure_callback): # variadic `typing.Tuple` are encodable if their generic type argument is encodable return True return _not_json_encodable("Type of elements in %s is not json-encodable."%str(t), failure_callback=failure_callback) else: if all(is_json_encodable(s, failure_callback=failure_callback) for s in t.__args__): # fixed-length `typing.Tuple` are encodable if all their generic type arguments are encodable return True return _not_json_encodable("Type of some element in %s is not json-encodable."%str(t), failure_callback=failure_callback) if t.__origin__ is Union: if all(is_json_encodable(s, failure_callback=failure_callback) for s in t.__args__): # `typing.Union` are encodable if all their generic type arguments are encodable return True return _not_json_encodable("Some type in %s is not json-encodable."%str(t), failure_callback=failure_callback) if t.__origin__ in (dict, OrderedDict, Mapping): # `typing.Dict`, `typing.OrderedDict` and `typing.Mapping` are encodable if their generic key and value types are encodable and their key type is keyable if not is_keyable(t.__args__[0], failure_callback=failure_callback): return _not_json_encodable("Type of keys in %s is not keyable."%str(t), failure_callback=failure_callback) if not is_json_encodable(t.__args__[0], failure_callback=failure_callback): return _not_json_encodable("Type of keys in %s is not json-encodable."%str(t), failure_callback=failure_callback) if not is_json_encodable(t.__args__[1], failure_callback=failure_callback): return _not_json_encodable("Type of values in %s is not json-encodable."%str(t), failure_callback=failure_callback) return True if t.__origin__ is Literal: # `typing_extensions.Literal` are encodable as long as their literals are JSON basic types, which is always the case if they are typecheckable. return True return False - if
def to_json_obj(obj: Any, t: Type, use_decimal: bool = False, typecheck: bool = True, namedtuples_as_lists=False) ‑> Any-
Encodes an instance
objof typecheckable typetinto a JSON object. The optionaluse_decimalparameter can be used to specify that instances ofdecimal.Decimalcan be used in the output: ifFalse, they are converted to strings. This method raisesTypeErrorif typetis not typecheckable according tois_typecheckable(). This method raisesTypeErrorifobjis not of typetaccording tois_instance().Currently, this method acts as follows on an instance
objof typet:- if
tis one of the JSON basic typesbool,int,float,str,NoneType, the instanceobjis returned unchanged; - if
tisdecimal.Decimalanduse_decimalisFalse(default),str(obj)is returned; - if
tisdecimal.Decimalanduse_decimalisTrue,objis returned unchanged; - if
tisNone(used as an alias forNoneType),Noneis returned; - if
tis an enum (i.e.isinstance(t, EnumMeta)), the enum value nameobj._name_is returned; - if
tis a namedtuple according tois_namedtuple()and all its fields are JSON encodable andnamedtuples_as_listsisFalse, this method is called recursively on all field values and then an ordered dictionary is returned with the field names as names and the JSON-encoded field values as corresponding values; - if
tis a namedtuple according tois_namedtuple()and all its fields are JSON encodable andnamedtuples_as_listsisTrue, this method is called recursively on all field values and then a list is returned with the JSON-encoded field values appearing in the same order as the namedtuple fields (which are not explicitly encoded); - if
tis a typed dict according tois_typed_dict()and all its values are JSON encodable, then a dictionary is returned with the same keys asobjand JSON-encoded values using the types specified byt. - if
tistyping.Union, the generic type arguments in the union are tried one after the other until auis found such thatis_instance(obj, u), thenobjis JSON-encoded usinguas its type. - if
tis atyping_extensions.Literal,objis returned unchanged; - if
tis one oftyping.List,typing.Set,typing.FrozenSet,typing.Dequeortyping.Tuple, a list is returned containing the elements of the original collection, recursively JSON-encoded; - if
tis atyping.Dictortyping.Mapping, a dictionary (dict) is returned with JSON-encoded values from the original dictionary/mapping, associated to either then JSON-encoded keys or a stringified version of the JSON-encoded keys (cf. below); - if
tistyping.OrderedDict, an ordered dictionary (collections.OrderedDict) is returned with JSON-encoded values from the original dictionary/mapping, associated to either then JSON-encoded keys or a stringified version of the JSON-encoded keys (cf. below).
In the case of dictionaries, it is not necessarily the case keys will be compatible with the JSON specification in their JSON-encoded form. When encoding dictionaries, the keys used in the encoding follow the following criteria:
- if the key type is a JOSN basic type,
decimal.Decimalor an enumeration type, the JSON encoding of the keys is used; - otherwise, the stringified version of the JSON encoding (using
json.dumps) is used;
Literals can only be of JSON basic type.
An optional parameter
typecheck(default:True) can be used to skip the check thattbe JSON encodable and thatobjbe an instance oft. The parametertypecheckis set toFalsein all recursive calls (i.e. typechecking is only done once).(Version 0.1.3)
Expand source code
def to_json_obj(obj: Any, t: Type, use_decimal: bool = False, typecheck: bool = True, namedtuples_as_lists=False) -> Any: """ Encodes an instance `obj` of typecheckable type `t` into a JSON object. The optional `use_decimal` parameter can be used to specify that instances of `decimal.Decimal` can be used in the output: if `False`, they are converted to strings. This method raises `TypeError` if type `t` is not typecheckable according to `typing_json.typechecking.is_typecheckable`. This method raises `TypeError` if `obj` is not of type `t` according to `typing_json.typechecking.is_instance`. Currently, this method acts as follows on an instance `obj` of type `t`: - if `t` is one of the JSON basic types `bool`, `int`, `float`, `str`, `NoneType`, the instance `obj` is returned unchanged; - if `t` is `decimal.Decimal` and `use_decimal` is `False` (default), `str(obj)` is returned; - if `t` is `decimal.Decimal` and `use_decimal` is `True`, `obj` is returned unchanged; - if `t` is `None` (used as an alias for `NoneType`), `None` is returned; - if `t` is an enum (i.e. `isinstance(t, EnumMeta)`), the enum value name `obj._name_` is returned; - if `t` is a namedtuple according to `typing_json.typechecking.is_namedtuple` and all its fields are JSON encodable and `namedtuples_as_lists` is `False`, this method is called recursively on all field values and then an ordered dictionary is returned with the field names as names and the JSON-encoded field values as corresponding values; - if `t` is a namedtuple according to `typing_json.typechecking.is_namedtuple` and all its fields are JSON encodable and `namedtuples_as_lists` is `True`, this method is called recursively on all field values and then a list is returned with the JSON-encoded field values appearing in the same order as the namedtuple fields (which are not explicitly encoded); - if `t` is a typed dict according to `typing_json.typechecking.is_typed_dict` and all its values are JSON encodable, then a dictionary is returned with the same keys as `obj` and JSON-encoded values using the types specified by `t`. - if `t` is `typing.Union`, the generic type arguments in the union are tried one after the other until a `u` is found such that `is_instance(obj, u)`, then `obj` is JSON-encoded using `u` as its type. - if `t` is a `typing_extensions.Literal`, `obj` is returned unchanged; - if `t` is one of `typing.List`, `typing.Set`, `typing.FrozenSet`, `typing.Deque` or `typing.Tuple`, a list is returned containing the elements of the original collection, recursively JSON-encoded; - if `t` is a `typing.Dict` or `typing.Mapping`, a dictionary (`dict`) is returned with JSON-encoded values from the original dictionary/mapping, associated to either then JSON-encoded keys or a stringified version of the JSON-encoded keys (cf. below); - if `t` is `typing.OrderedDict`, an ordered dictionary (`collections.OrderedDict`) is returned with JSON-encoded values from the original dictionary/mapping, associated to either then JSON-encoded keys or a stringified version of the JSON-encoded keys (cf. below). In the case of dictionaries, it is not necessarily the case keys will be compatible with the JSON specification in their JSON-encoded form. When encoding dictionaries, the keys used in the encoding follow the following criteria: - if the key type is a JOSN basic type, `decimal.Decimal` or an enumeration type, the JSON encoding of the keys is used; - otherwise, the stringified version of the JSON encoding (using `json.dumps`) is used; Literals can only be of JSON basic type. An optional parameter `typecheck` (default: `True`) can be used to skip the check that `t` be JSON encodable and that `obj` be an instance of `t`. The parameter `typecheck` is set to `False` in all recursive calls (i.e. typechecking is only done once). (Version 0.1.3) """ # pylint:disable=invalid-name,too-many-return-statements,too-many-branches if typecheck: trace: List[str] = [] def failure_callback(message: str) -> None: trace.append(message) if not is_json_encodable(t, failure_callback=failure_callback): # Argument `t` must be JSON encodable. raise TypeError("Type %s is not json-encodable. Trace:\n%s"%(str(t), "\n".join(trace))) trace = [] if not is_instance(obj, t, failure_callback=failure_callback): # Argument `obj` must be an instance of argument `t`. raise TypeError("Object %s is not of type %s. Trace:\n%s"%(short_str(obj), str(t), "\n".join(trace))) if t in JSON_BASE_TYPES: # JSON basic types are returned unchanged. return obj if t is Decimal: # If `use_decimal` is `True`, `obj` is returned unchanged: if use_decimal: return obj # If `use_decimal` is `False` (default), instances of `decimal.Decimal` are encoded as strings. return str(obj) if t in (None, type(None)): # `None` can be used as an alias for `NoneType`. return None if isinstance(t, EnumMeta): # Enum values are encoded by their name. return obj._name_ # pylint:disable=protected-access if is_namedtuple(t): # Namedtuples are encoded as ordered dictionaries, with their fields as keys and the JSON-encoded field values as corresponding values. field_types = getattr(t, "_field_types") return _to_json_obj_namedtuple(obj, field_types, use_decimal=use_decimal, namedtuples_as_lists=namedtuples_as_lists) if is_typed_dict(t): # Typed dicts are encoded as ordered dictionaries, with their fields as keys and the JSON-encoded field values as corresponding values. field_types = getattr(t, "__annotations__") # return _to_json_obj_namedtuple(obj, field_types, use_decimal=use_decimal, namedtuples_as_lists=namedtuples_as_lists) # A `dict`is used for `typing.Dict` and `typing.Mapping`. return { field: to_json_obj(obj[field], field_type, use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for field, field_type in field_types.items() } if hasattr(t, "__origin__") and hasattr(t, "__args__"): # Generics from the `typing` module. if t.__origin__ is Union: # values in a `typing.Union` are JSON-encoded using the first type in the union that the object is found to be an instance of. for s in t.__args__: if is_instance(obj, s): return to_json_obj(obj, s, use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) raise AssertionError(_UNREACHABLE_ERROR_MSG) # pragma: no cover if t.__origin__ is Literal: # `typing_extensions.Literal` are returned unchanged return obj if t.__origin__ in (list, set, frozenset, deque): # `typing.List`, `typing.Set`, `typing.FrozenSet` and `typing.Deque` are turned into lists, with their elements recursively JSON-encoded return _to_json_obj_homogeneous_collection(obj, t.__args__[0], use_decimal=use_decimal, namedtuples_as_lists=namedtuples_as_lists) if t.__origin__ is tuple: # `typing.Tuple` are turned into lists, with their elements recursively JSON-encoded if len(t.__args__) == 2 and t.__args__[1] is ...: # pylint:disable=no-else-return return _to_json_obj_homogeneous_collection(obj, t.__args__[0], use_decimal=use_decimal, namedtuples_as_lists=namedtuples_as_lists) else: return [to_json_obj(x, t.__args__[i], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for i, x in enumerate(obj)] if t.__origin__ in (dict, OrderedDict, Mapping): # `typing.Dict` and `typing.Mapping` are turned into dictionaries and `typing.OrderedDict` are turned into ordered dictionaries. # The values are recursively JSON-encoded. Keys require special handling. fields = [field for field in obj] # pylint: disable = unnecessary-comprehension if t.__args__[0] in JSON_BASE_TYPES+(Decimal, None,): # Keys of JSON basic types, `decimal.Decimal` and `None` are recursively JSON-encoded. # encoded_fields = [field for field in fields] # pylint: disable = unnecessary-comprehension encoded_fields = [to_json_obj(field, t.__args__[0], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for field in fields] elif (hasattr(t.__args__[0], "__origin__") and t.__args__[0].__origin__ is Literal): # Keys of `typing_extensions.Literal` types are recursively JSON-encoded. # encoded_fields = [field for field in fields] # pylint: disable = unnecessary-comprehension encoded_fields = [to_json_obj(field, t.__args__[0], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for field in fields] elif isinstance(t.__args__[0], EnumMeta): # Keys of enumeration types are recursively JSON-encoded. encoded_fields = [to_json_obj(field, t.__args__[0], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for field in fields] else: # Keys of any other type are recursively JSON-encoded and then JSON dumped to strings. encoded_fields = [json.dumps(to_json_obj(field, t.__args__[0], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists)) for field in fields] if t.__origin__ in (dict, Mapping): # A `dict`is used for `typing.Dict` and `typing.Mapping`. return {encoded_fields[i]: to_json_obj(obj[field], t.__args__[1], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) for i, field in enumerate(fields)} if t.__origin__ is OrderedDict: # A `collections.OrderedDict` is used for `typing.OrderedDict`. new_ordered_dict = OrderedDict() # type:ignore for i, field in enumerate(fields): new_ordered_dict[encoded_fields[i]] = to_json_obj(obj[field], t.__args__[1], use_decimal=use_decimal, typecheck=False, namedtuples_as_lists=namedtuples_as_lists) return new_ordered_dict raise AssertionError(_UNREACHABLE_ERROR_MSG) # pragma: no cover - if