Enhancing JSON Serialization In Python With Typing

Maintaining valid types when deserializing JSON with Python is hard. This article discusses an approach using typing. You can find the package on GitHub.

The Problem

Here we have a python dict with a mix of data types.

from datetime import datetime
from decimal import Decimal

dct = {
'some_text': 'Hello, World!',
'some_date': datetime.fromisoformat('2020-01-29T12:56:13'),
'some_int': 42,
'some_float': 3.14,
'some_decimal': Decimal("2.414"),
'some_list': [
{
'other_text': 'Hello, World!',
'other_date': datetime.fromisoformat('2020-01-29T12:56:13'),
'other_decimal': Decimal("2.414")
},
{
'other_text': 'Hello, World!',
'other_date': datetime.fromisoformat('2020-01-29T12:56:13'),
'other_decimal': Decimal('2.414')
}
]
}
import json

try:
text = json.dumps(dct)
except Exception as error:
print(error)

Object of type datetime is not JSON serializable
class MyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, Decimal):
return float(obj)
text = json.dumps(dct, cls=MyEncoder, indent=4)
print(text)
{
"some_text": "Hello, World!",
"some_date": "2020-01-29T12:56:13",
"some_int": 42,
"some_float": 3.14,
"some_decimal": 2.414,
"some_list": [
{
"other_text": "Hello, World!",
"other_date": "2020-01-29T12:56:13",
"other_decimal": 2.414
},
{
"other_text": "Hello, World!",
"other_date": "2020-01-29T12:56:13",
"other_decimal": 2.414
}
]
}
obj1 = json.loads(text)
print(obj1)
{
'some_text': 'Hello, World!',
'some_date': '2020-01-29T12:56:13',
'some_int': 42,
'some_float': 3.14,
'some_decimal': 2.414,
'some_list': [
{
'other_text': 'Hello, World!',
'other_date': '2020-01-29T12:56:13',
'other_decimal': 2.414
},
{
'other_text': 'Hello, World!',
'other_date': '2020-01-29T12:56:13',
'other_decimal': 2.414
}
]
}
print(dct == obj1)
False
def object_hook(dct):
for key, value in dct.items():
try:
dct[key] = datetime.fromisoformat(value)
except:
pass
return dct
obj2 = json.loads(text, object_hook=object_hook)
print(obj2)
{
'some_text': 'Hello, World!',
'some_date': datetime.datetime(2020, 1, 29, 12, 56, 13),
'some_int': 42,
'some_float': 3.14,
'some_decimal': 2.414,
'some_list': [
{
'other_text': 'Hello, World!',
'other_date': datetime.datetime(2020, 1, 29, 12, 56, 13),
'other_decimal': 2.414
},
{
'other_text': 'Hello, World!',
'other_date': datetime.datetime(2020, 1, 29, 12, 56, 13),
'other_decimal': 2.414
}
]
}
print(dct == obj2)
False

Type Annotations

With type annotations we should be able to solve this problem. The data structure we are streaming can be “typed” by using a TypedDict. in Python 3.7 this can be found in the typing_extensions package, while in 3.8 it is standard.

from typing import List
try:
from typing import TypedDict
except:
from typing_extensions import TypedDict

class InnerDict(TypedDict):
other_text: str
other_date: datetime
other_decimal: Decimal

class OuterDict(TypedDict):
some_text: str
some_date: datetime
some_int: int
some_float: float
some_decimal: Decimal
some_list: List[InnerDict]
from jetblack_serialization.config import SerializerConfig
from jetblack_serialization.json import deserialize, serialize
from stringcase import camelcase, snakecase

config = SerializerConfig(camelcase, snakecase, pretty_print=True)

text = serialize(dct, OuterDict, config)
print(text)
{
"someText": "Hello, World!",
"someDate": "2020-01-29T12:56:13.00Z",
"someInt": 42,
"someFloat": 3.14,
"someDecimal": 2.414,
"someList": [
{
"otherText": "Hello, World!",
"otherDate": "2020-01-29T12:56:13.00Z",
"otherDecimal": 2.414
},
{
"otherText": "Hello, World!",
"otherDate": "2020-01-29T12:56:13.00Z",
"otherDecimal": 2.414
}
]
}
obj = deserialize(text, OuterDict, config)
print(obj)
{
'some_text': 'Hello, World!',
'some_date': datetime.datetime(2020, 1, 29, 12, 56, 13),
'some_int': 42,
'some_float': 3.14,
'some_decimal': Decimal('2.414'),
'some_list': [
{
'other_text': 'Hello, World!',
'other_date': datetime.datetime(2020, 1, 29, 12, 56, 13),
'other_decimal': Decimal('2.414')
},
{
'other_text': 'Hello, World!',
'other_date': datetime.datetime(2020, 1, 29, 12, 56, 13),
'other_decimal': Decimal('2.414')
}
]
}
print(dct == obj)
True
from typing_extensions import Annotated
from jetblack_serialization.xml import serialize as serialize_xml, XMLEntity
from stringcase import pascalcase

xml_config = SerializerConfig(pascalcase, snakecase, pretty_print=True)

text = serialize_xml(dct, Annotated[OuterDict, XMLEntity('Dict')], xml_config)
print(text)
<Dict>
<SomeText>Hello, World!</SomeText>
<SomeDate>2020-01-29T12:56:13.00Z</SomeDate>
<SomeInt>42</SomeInt>
<SomeFloat>3.14</SomeFloat>
<SomeDecimal>2.414</SomeDecimal>
<SomeList>
<OtherText>Hello, World!</OtherText>
<OtherDate>2020-01-29T12:56:13.00Z</OtherDate>
<OtherDecimal>2.414</OtherDecimal>
</SomeList>
<SomeList>
<OtherText>Hello, World!</OtherText>
<OtherDate>2020-01-29T12:56:13.00Z</OtherDate>
<OtherDecimal>2.414</OtherDecimal>
</SomeList>
</Dict>

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store