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
t
can be encoded into JSON (or decoded from JSON) using thetyping_json
library.The optional parameter
failure_callback
can be used to collect a detailed trace of the reasons behind this method returningFalse
on a given typet
.Currently, a type
t
is JSON encodable according to this method if it is typecheckable according tois_typecheckable()
and it satisfies one of the following conditions:- if
t
is one of the JSON basic typesbool
,int
,float
,str
,NoneType
; - if
t
is adecimal.Decimal
; - if
t
isNone
(used as an alias forNoneType
); - if
t
is an enum (i.e.isinstance(t, EnumMeta)
); - if
t
is a namedtuple according tois_namedtuple()
and all its fields are JSON encodable; - if
t
is a typed dictionary according tois_typed_dict()
and all its values are JSON encodable; - if
t
is one oftyping.List
,typing.Set
,typing.FrozenSet
,typing.Deque
,typing.Optional
or a variadictyping.Tuple
and its generic type argument is JSON encodable; - if
t
is atyping.Union
or a fixed-lengthtyping.Tuple
and all of its generic type arguments are JSON encodable; - if
t
is atyping.Dict
,typing.OrderedDict
ortyping.Mapping
, its generic key type is keyable (according tois_keyable()
) and both its generic key and value types are JSON encodable; - if
t
is atyping_extensions.Literal
and 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
obj
of typecheckable typet
into a JSON object. The optionaluse_decimal
parameter can be used to specify that instances ofdecimal.Decimal
can be used in the output: ifFalse
, they are converted to strings. This method raisesTypeError
if typet
is not typecheckable according tois_typecheckable()
. This method raisesTypeError
ifobj
is not of typet
according tois_instance()
.Currently, this method acts as follows on an instance
obj
of typet
:- if
t
is one of the JSON basic typesbool
,int
,float
,str
,NoneType
, the instanceobj
is returned unchanged; - if
t
isdecimal.Decimal
anduse_decimal
isFalse
(default),str(obj)
is returned; - if
t
isdecimal.Decimal
anduse_decimal
isTrue
,obj
is returned unchanged; - if
t
isNone
(used as an alias forNoneType
),None
is returned; - if
t
is an enum (i.e.isinstance(t, EnumMeta)
), the enum value nameobj._name_
is returned; - if
t
is a namedtuple according tois_namedtuple()
and all its fields are JSON encodable andnamedtuples_as_lists
isFalse
, 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 tois_namedtuple()
and all its fields are JSON encodable andnamedtuples_as_lists
isTrue
, 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 tois_typed_dict()
and all its values are JSON encodable, then a dictionary is returned with the same keys asobj
and JSON-encoded values using the types specified byt
. - if
t
istyping.Union
, the generic type arguments in the union are tried one after the other until au
is found such thatis_instance(obj, u)
, thenobj
is JSON-encoded usingu
as its type. - if
t
is atyping_extensions.Literal
,obj
is returned unchanged; - if
t
is one oftyping.List
,typing.Set
,typing.FrozenSet
,typing.Deque
ortyping.Tuple
, a list is returned containing the elements of the original collection, recursively JSON-encoded; - if
t
is atyping.Dict
ortyping.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
istyping.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 thatt
be JSON encodable and thatobj
be an instance oft
. The parametertypecheck
is set toFalse
in 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