Welcome to jsonype’s documentation!

Contents:

jsonype is a package for converting Python’s JSON representation to (or from) a Python object of a given type if possible (i.e. a suitable converter is available). This is most useful when the given type contains type-hints such that this type-based conversion can be applied (possibly recursively) for the individual components of the initial type.

Install

Add jsonype to your dependencies or install with pip:

pip install jsonype

Usage

Conversion of standard types like dataclasses.dataclass() or typing.NamedTuple works out of the box:

>>> from dataclasses import dataclass
>>> from typing import NamedTuple
>>> from jsonype import TypedJson, FromJsonConversionError, JsonPath
>>> from json import dumps, loads
>>>
>>> # Create TypedJson instance
>>> typed_json = TypedJson.default()
>>>
>>> # Define your types with type-hints
>>> class Address(NamedTuple):
...     street: str
...     city: str
...     some_related_number: int
>>>
>>> @dataclass
... class Person:
...     name: str
...     address: Address
>>>
>>> # Parse JSON string with python's json package
>>> js = loads('''{
...         "name": "John Doe",
...         "address": {
...             "street": "123 Maple Street",
...             "city": "Any town",
...             "some_related_number": 5,
...             "zip": "ignored"
...         }
...     }''')
>>> # convert generic representation to your type
>>> person = typed_json.from_json(js, Person)
>>>
>>> assert person == Person(
...     name="John Doe",
...     address=Address(
...         street="123 Maple Street",
...         city="Any town",
...         some_related_number=5
...     ),
... )
>>>
>>> try:
...     # strict conversion does not accept extra fields in the JSON-object
...     person = TypedJson.default(strict=True).from_json(js, Person)
... except FromJsonConversionError as e:
...     print(e)
("Cannot convert {'street': '...', ..., 'zip': 'ignored'} (type: <class 'dict'>)
at $.address to <class 'Address'>: unexpected keys: {'zip'}", ...
>>>
>>> # JSON-types must match expected types:
>>> # FromJsonConversionError contains path where the error occurred.
>>> js = loads('''{
...         "name": "John Doe",
...         "address": {
...             "street": "123 Maple Street",
...             "city": "Any town",
...             "some_related_number": "5"
...         }
...     }''')
>>> try:
...     person = typed_json.from_json(js, Person)
... except FromJsonConversionError as e:
...     print(e)
...     assert e.path == JsonPath(("address", "some_related_number"))
("Cannot convert 5 (type: <class 'str'>)
at $.address.some_related_number to <class 'int'>...", ...
>>> # Convert typed objects to JSON
>>> print(dumps(typed_json.to_json(person), indent=2))
{
  "name": "John Doe",
  "address": {
    "street": "123 Maple Street",
    "city": "Any town",
    "some_related_number": 5
  }
}

For custom types you can register custom converters:

>>> from dataclasses import dataclass
>>> from typing import Callable, Any
>>> from jsonype import (TypedJson, FromJsonConversionError, FromJsonConverter,
...     JsonPath, Json, ParameterizedTypeInfo)
>>> from json import dumps, loads
>>>
>>> # A custom type that needs a custom converter
>>> class CustomType:
...     def __eq__(self, other: Any) -> bool:
...         return type(other) == CustomType
>>>
>>> @dataclass
... class Person:
...     name: str
...     something_special: CustomType
>>>
>>> js = loads('''{
...     "name": "John Doe",
...     "something_special": "CustomType"
... }''')
>>> typed_json = TypedJson.default()
>>> # Without custom converter the conversion fails
>>> try:
...     person = typed_json.from_json(js, Person)
... except FromJsonConversionError as e:
...     print(e)
("Cannot convert CustomType (type: <class 'str'>) at $.something_special
to <class 'CustomType'>: No suitable converter registered.
Use TypedJson.append or TypedJson.prepend to register one.", ...
>>> # Let's write a custom converter that can convert the String "CustomType" to
>>> # an instance of the CustomType ...
>>> class StringToCustomType(FromJsonConverter[CustomType, None]):
...
...     def can_convert(
...         self, js: Json, target_type_info: ParameterizedTypeInfo[Any]
...     ) -> bool:
...         return (js == CustomType.__name__
...             and target_type_info.full_type is CustomType)
...
...     def convert(self, js: Json, target_type_info: ParameterizedTypeInfo[CustomType],
...         path: JsonPath,
...         from_json: Callable[[Json, type[None], JsonPath],
...         None]
...     ) -> CustomType:
...         return CustomType()
>>>
>>> # ... and create a new TypedJson instance with the converter appended.
>>> typed_json = typed_json.append([StringToCustomType()], [])
>>> person = typed_json.from_json(js, Person)
>>> assert person == Person("John Doe", CustomType())

Custom converters can also take precedence over existing ones by prepending:

>>> from dataclasses import dataclass
>>> from jsonype import TypedJson, FunctionBasedToSimpleJsonConverter
>>> from json import dumps
>>>
>>> class Password(str):
...     pass
>>>
>>> @dataclass
... class Person:
...     name: str
...     pwd: Password
>>>
>>> person = Person("John Doe", Password("secret"))
>>>
>>> typed_json = TypedJson.default()
>>> # The secret is revealed
>>> print(dumps(typed_json.to_json(person)))
{"name": "John Doe", "pwd": "secret"}
>>> # A custom converter can prevent revealing Password types
>>> # Simple custom converters are most easily built with
>>> # FunctionBasedFromSimpleJsonConverter or FunctionBasedToSimpleJsonConverter
>>> password_to_str = FunctionBasedToSimpleJsonConverter(lambda pwd: "***", Password)
>>> # Since a Password is also a str the new converter needs to take precedence over
>>> # the existing converter for str, that is why it is prepended.
>>> typed_json = typed_json.prepend([], [password_to_str])
>>> print(dumps(typed_json.to_json(person)))
{"name": "John Doe", "pwd": "***"}

See jsonype.TypedJson for more details on the API.

Indices and tables