datetimeやsetのJSON化

Object of type ・・・ is not JSON serializable

日付やsetなどを含む辞書をjsonモジュールのdumpでJSON文字列に変換すると、「Object of type ・・・ is not JSON serializable」というエラーが発生します。

import datetime
import json

data = dict()
data["id"] = 1
data["datetime"] = datetime.datetime.now()

json.dumps(data)    # Object of type datetime is not JSON serializable

JSONは型として日時を表す型がサポートされていないため、明示的にどういう文字列に変換するか、というロジックを指定する必要があります。なおJSONでサポートされている型は以下のとおりです。

  • オブジェクト(辞書)
  • 配列(リスト)
  • 数値
  • 文字列
  • 真理値
  • null

https://www.json.org/json-en.html

datetimeやsetをJSON化する方法

理屈は後回しにし、まず先にdatetimeやsetを含んだ辞書をJSON化する方法から解説します。以下のコードのようにjson.dump関数の引数のdefaultに、サポート外の型が指定された場合の挙動を関数として指定するとJSON文字列として変換することが可能となります。

import json
from datetime import datetime, date

# サポート外の型が指定されたときの挙動を定義
def custom_default(o):
    if hasattr(o, '__iter__'):
        # イテラブルなものはリストに
        return list(o)
    elif isinstance(o, (datetime, date)):
        # 日時の場合はisoformatに
        return o.isoformat()
    else:
        # それ以外は文字列に
        return str(o)

# 通常JSONにできないオブジェクトを含む辞書
mydict = {"int": 100,
        "str": "My String",
        "set": {1, 3, 5},
        "list": [1, 2, 3, 4, 5],
        "dict": {"key1": 100, "key2": 200},
        "range": range(10),
        "datetime": datetime.now(),
        "date": date.today(),
        "function": lambda x: x}

json.dumps(mydict, default=custom_default)
# {"int": 100, "str": "My String", "set": [1, 3, 5], "list": [1, 2, 3, 4, 5], "dict": {"key1": 100, "key2": 200}, "range": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "datetime": "2022-02-27T21:11:36.683126", "date": "2022-02-27", "function": "<function <lambda> at 0x000001253A540820>"}

上のコードではイテラブルなものはリストに、日付の場合はiso形式に、それ以外は文字列に変換するように指定しています。

JSONDecoder

ここから内部的な仕組みについて解説します。json.dump()は内部でJSONDecoderというクラスを使用しますが、その処理で以下のdefaultメソッドを呼び出します。デフォルトでは以下の実装の通りサポート外の変数が来た際、問答無用でTypeErrorが発生するように実装されています。ここをカスタマイズすることで特定の型の場合の挙動を指定することが可能となります。

# Python\Python3.8\Lib\json\encoder.pyより抜粋

def default(self, o):
        """Implement this method in a subclass such that it returns
        a serializable object for ``o``, or calls the base implementation
        (to raise a ``TypeError``).

        For example, to support arbitrary iterators, you could
        implement default like this::

            def default(self, o):
                try:
                    iterable = iter(o)
                except TypeError:
                    pass
                else:
                    return list(iterable)
                # Let the base class default method raise the TypeError
                return JSONEncoder.default(self, o)

        """
        raise TypeError(f'Object of type {o.__class__.__name__} '
                        f'is not JSON serializable')

また、先程のdatetime、setをJSON化するコードですが以下のようにJSONEncoderを継承してdefaultメソッドをオーバーライドしても構いません。この場合は以下のコードのようにjson.dumpsの引数のclsにカスタマイズしたクラスを指定します。

import json
from datetime import datetime, date

class MyEncoder(json.JSONEncoder):
    def default(self, o):
        if hasattr(o, '__iter__'):
            # イテレータを持つ場合
            return list(o)
        elif isinstance(o, (datetime, date)):
            # 日時の場合
            return o.isoformat()
        else:
            return str(o)


# 引数clsを指定する
json.dumps(mydict, cls=MyEncoder)