Skip to content

Serializing JSON

Given a typed dictionary:

from datetime import datetime
from typing import Optional, TypedDict, Union

class Book(TypedDict, total=False):
    book_id: int
    title: str
    author: str
    publication_date: datetime
    keywords: list[str]
    phrases: list[str]
    age: Optional[Union[datetime, int]]
    pages: Optional[int]

Create some data:

obj: Book = {
    'author': 'Chairman Mao',
    'book_id': 42,
    'title': 'Little Red Book',
    'publication_date': datetime(1973, 1, 1, 21, 52, 13),
    'keywords': ['Revolution', 'Communism'],
    'phrases': [
        'Revolutionary wars are inevitable in class society',
        'War is the continuation of politics'
    ],
    'age': 24,
}

Serializing

This could be serialized to JSON as:

from stringcase import camelcase, snakecase
from jetblack_serialization.json import serialize, SerializerConfig

text = serialize(
    obj,
    Book,
    SerializerConfig(key_serializer=camelcase, pretty_print=True)
)
print(text)

giving:

{
    "bookId": 42,
    "title": "Little Red Book",
    "author": "Chairman Mao",
    "publicationDate": "1973-01-01T21:52:13.00Z",
    "keywords": ["Revolution", "Communism"],
    "phrases": ["Revolutionary wars are inevitable in class society", "War is the continuation of politics"],
    "age": 24,
    "pages": null
}

Note the fields have been camel cased, and the publication date has been turned into an ISO 8601 date.

Deserializing

We can deserialize the data as follows:

from stringcase import camelcase, snakecase
from jetblack_serialization.json import deserialize, SerializerConfig

dct = deserialize(
    text,
    Annotated[Book, JSONValue()],
    SerializerConfig(key_deserializer=snakecase)
)

Attributes

For JSON, attributes are typically not required. However JSONProperty, JSONObject and JSONValue() are provided for completeness.

Resolving Unions With Type Selectors

The default behavior when handling a union is to attempt each element and accept the first valid attempt. This is obviously inefficient, and potentially inaccurate. We can use type selectors to sort this out.

Given the following schema:

from typing import Annotated, Any, Literal, TypedDict

from stringcase import snakecase, camelcase

from jetblack_serialization import Annotation, SerializerConfig
from jetblack_serialization.json import (
    serialize_typed,
    deserialize_typed,
    JSONValue,
)

CONFIG = SerializerConfig(
    key_serializer=camelcase,
    key_deserializer=snakecase,
)


class ShapeBase(TypedDict):
    name: str
    shape_type: Literal['circle', 'rectangle']


class ShapeCircle(ShapeBase):
    radius: float


class ShapeRectangle(ShapeBase):
    width: float
    height: float


type Shape = ShapeCircle | ShapeRectangle

A type selector is given the data that is about to be serialized (or deserialized), the type annotation, a boolean flag indicating whether the value is being serialized or deserialized, and the serializer config.

In the example below we use the 'shape_type' property to return the appropriate type.

def select_shape_type(
        data: Any,
        type_annotation: Annotation,
        is_serializing: bool,
        config: SerializerConfig
) -> Annotation:
    assert isinstance(data, dict)
    key = 'shape_type'
    tag = key if is_serializing else config.serialize_key(key)
    match data.get(tag):
        case 'circle':
            return ShapeCircle
        case 'rectangle':
            return ShapeRectangle
        case _:
            raise ValueError(f"Unknown shape type: {data.get(tag)}")

A type selector can be passed to any of the JSON annotations using the type_selector keyword argument.

The example below uses JSONValue.

class Button(TypedDict):
    title: str
    shape: Annotated[
        Shape,
        JSONValue(type_selector=select_shape_type)
    ]

Problematic Tag Names

Sometimes a tag name is something that is awkward to handle in python. This might be a keyword, or contain an unrepresentable character. The JSONProperty annotation can be used to specify the tag.

class Schema(TypedDict):
    ref: Annotated[str, JSONProperty("$ref")]

Value Style Keys

Sometimes property names or keys are actually values. For example in an HTTP messages the headers can be considered keys; however they should not be converted between camel-case and snake-case, as their values have meaning. The property is_serializable_keys is available in JSONProperty and JSONObject.

class HttpRequest(TypedDict):
    method: Literal['GET', 'POST']
    headers: Annotated[dict[str, Any], JSONObject(is_serializable_keys=False)]