DynamoDB JSONの型変換をboto3だけで行ってみる

2023.01.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんばんは、CX事業本部Delivery部の夏目です。

本日boto3がResource Interfaceを廃止する方向というニュースが飛び込んで来まして、早速DynamoDBのPutとGetをClient Interfaceで行うブログが投稿されました。
これはDynamoDBにおいてClient Interfaceを使用使用するとDynamoDB JSONという特殊な形式でデータが返ってくるため使い勝手が悪く、一般的にResource Interfaceが使用されていたので急ぎ記事にした、というもの(と私は解釈している)。

boto3のclientでDynamoDBテーブルを利用してみる(scan, put_item, get_item, update_item, delete_item)

この記事において、DynamoDB JSON形式では型情報を持たせる必要があるため、dictからの型変換が大変だという記述があります。

DynamoDBテーブルの型情報が必要なのがとても手間です。 その代替策として、とりあえず、2つありそうです。

https://dev.classmethod.jp/articles/boto3-client-dynamodb-sample/#toc-10

記事では外部のライブラリを使用する方法を紹介しているのですが、実はboto3の中に型変換のためのクラスが存在しているので紹介します。

from boto3.dynamodb.types import TypeSerializer, TypeDeserializer

使うのはTypeSerializerTypeDeserializerです。

わかりやすく言うと、TypeSerializerがdictからDynamoDB JSONへの変換を、TypeDeserializerがDynamoDB JSONからdictへの変換を、行うためのクラスです。 正確には、Pythonの値をDynamoDB JSONの値にSerializeしたり、DynamoDB JSONの値をPythonの値にDeserializeしたりする、クラスです。

言葉ではわかりにくいので実際に動かしてみます。

TypeSerializerの使い方

コード

from boto3.dynamodb.types import TypeSerializer

serializer = TypeSerializer()

item_python_dict = {
    "todoId": "t0001",
    "title": "this is title",
    "status": "unstarted",
    "createdAt": 1674124101857,
    "binary": b"pen",
    "any string set": {"Tag Name1", "Tag Name2", "TagName3"},
    "any number set": {111, 222, 333},
    "any binary set": [b"aaa", b"bbb", b"ccc"],
    "any map": {"map-name1": "map-value1", "map-name2": 111},
    "any list": ["list-value1", "222"],
    "any null": None,
    "any bol": False,
}

item_dynamodb_json = {
    k: serializer.serialize(v)
    for k, v in item_python_dict.items()
}

print(json.dumps(item_dynamodb_json, indent=4))

実行結果

{'todoId': {'S': 't0001'}, 'title': {'S': 'this is title'}, 'status': {'S': 'unstarted'}, 'createdAt': {'N': '1674124101857'}, 'binary': {'B': b'pen'}, 'any string set': {'SS': ['Tag Name1', 'Tag Name2', 'TagName3']}, 'any number set': {'NS': ['333', '222', '111']}, 'any binary set': {'L': [{'B': b'aaa'}, {'B': b'bbb'}, {'B': b'ccc'}]}, 'any map': {'M': {'map-name1': {'S': 'map-value1'}, 'map-name2': {'N': '111'}}}, 'any list': {'L': [{'S': 'list-value1'}, {'S': '222'}]}, 'any null': {'NULL': True}, 'any bol': {'BOOL': False}}

実行結果 (整形済み)

{
    'todoId': {'S': 't0001'},
    'title': {'S': 'this is title'},
    'status': {'S': 'unstarted'},
    'createdAt': {'N': '1674124101857'},
    'binary': {'B': b'pen'},
    'any string set': {
        'SS': ['Tag Name1', 'Tag Name2', 'TagName3']
    },
    'any number set': {
        'NS': ['333', '222', '111']
    },
    'any binary set': {
        'L': [{'B': b'aaa'}, {'B': b'bbb'}, {'B': b'ccc'}]
    },
    'any map': {
        'M': {
            'map-name1': {'S': 'map-value1'},
            'map-name2': {'N': '111'}
        }
    },
    'any list': {
        'L': [{'S': 'list-value1'}, {'S': '222'}]
    },
    'any null': {'NULL': True},
    'any bol': {'BOOL': False}
}

コードの20から23行目を見るとわかるのですが、TypeSerializer().serialize()にはdict全体を渡さず、Key-ValueのValueだけ渡していることがわかります。

TypeSerializerはPythonで扱う型の値をDynamoDB JSONの型の値に変換するものです。

TypeDeserializer

TypeDeserializerTypeSerializerと逆のことを行うので、DynamoDB JSONの値をPythonの型の値に変換するクラスです。

コード

from boto3.dynamodb.types import TypeDeserializer

deserializer = TypeDeserializer()

item_dynamodb_json = {
    'todoId': {'S': 't0001'},
    'title': {'S': 'this is title'},
    'status': {'S': 'unstarted'},
    'createdAt': {'N': '1674124101857'},
    'binary': {'B': b'pen'},
    'any string set': {
        'SS': ['Tag Name1', 'Tag Name2', 'TagName3']
    },
    'any number set': {
        'NS': ['333', '222', '111']
    },
    'any binary set': {
        'L': [{'B': b'aaa'}, {'B': b'bbb'}, {'B': b'ccc'}]
    },
    'any map': {
        'M': {
            'map-name1': {'S': 'map-value1'},
            'map-name2': {'N': '111'}
        }
    },
    'any list': {
        'L': [{'S': 'list-value1'}, {'S': '222'}]
    },
    'any null': {'NULL': True},
    'any bol': {'BOOL': False}
}

item_python_dict = {
    k: deserializer.deserialize(v)
    for k, v in item_dynamodb_json.items()
}

print(item_python_dict)

実行結果

{'todoId': 't0001', 'title': 'this is title', 'status': 'unstarted', 'createdAt': Decimal('1674124101857'), 'binary': Binary(b'pen'), 'any string set': {'TagName3', 'Tag Name1', 'Tag Name2'}, 'any number set': {Decimal('333'), Decimal('222'), Decimal('111')}, 'any binary set': [Binary(b'aaa'), Binary(b'bbb'), Binary(b'ccc')], 'any map': {'map-name1': 'map-value1', 'map-name2': Decimal('111')}, 'any list': ['list-value1', '222'], 'any null': None, 'any bol': False}

実行結果 (整形済み)

{
    'todoId': 't0001',
    'title': 'this is title',
    'status': 'unstarted',
    'createdAt': Decimal('1674124101857'),
    'binary': Binary(b'pen'),
    'any string set': {'TagName3', 'Tag Name2', 'Tag Name1'},
    'any number set': {Decimal('333'), Decimal('222'), Decimal('111')},
    'any binary set': [Binary(b'aaa'), Binary(b'bbb'), Binary(b'ccc')],
    'any map': {
        'map-name1': 'map-value1', 'map-name2': Decimal('111')
    },
    'any list': ['list-value1', '222'],
    'any null': None,
    'any bol': False
}

こちらもTypeDeserializer().deserializer()に渡しているのは、DynamoDB JSON全体ではなく、Key-ValueのValueだけです。

Deserializeの際には完全にPythonの型に変換されるのではなく、バイナリの値だけはboto3.dynamodb.types.Binaryに変換されます。
(boto3で定義されているラッパー型。Binary(b'pen').valueで実際の値にアクセスできる)

まとめ

以上、DynamoDB JSONを変換するためのコードはboto3に既に実装されている、ということです。

実は、Resource Clientを使用できる現状でも使い道がありまして、DynamoDB StreamをトリガーとするLambdaのeventにはDynamoDB JSON形式でデータが格納されているのでTypeDeserializerにはお世話になることがあります。

Resource Interfaceが完全に廃止された際にTypeSerializerとTypeDeserializerも削除されるということがなければ、boto3だけで型変換もできるはずです。

P.S.

DynamoDBを使用する上で、boto3ではReource Interfaceにおいてbatch_writer()もよく使います。
こちらは自分の知る限りResource Interfaceでしか使えないので廃止の際にClientに移植されないかなぁと思ったりしています。

参考

https://boto3.amazonaws.com/v1/documentation/api/latest/_modules/boto3/dynamodb/types.html