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 typing import Callable, Any
>>> from jsonype import TypedJson, ToJsonConverter
>>> 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
>>> class PasswordToString(ToJsonConverter[str]):
... def can_convert(self, o: Any)-> bool:
... return isinstance(o, Password)
...
... def convert(self, o: Password, to_json: Callable[[Any], Json])-> Json:
... return "***"
>>>
>>> # 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([], [PasswordToString()])
>>> print(dumps(typed_json.to_json(person)))
{"name": "John Doe", "pwd": "***"}
See jsonype.TypedJson for more details on the API.