【python3】json でdatetime のisoformatで読み書きする

python で辞書型のデータをjson でやりとりすることが多いのですが、辞書の値ではdatetime で保持し、文字列やファイルにするときは```"YYYY-MM-DD HH-MN-SS"```という文字列で扱うようにする方法をメモします。

## datetime <--> 文字列

datetime のisoformat という関数で文字列にできます。separator を空白にして使うことにします。

```[python]
In [1]: from datetime import datetime

In [2]: t1 = datetime.now()

In [3]: t1.isoformat()
Out[3]: '2020-05-08T22:16:39.287433'

In [4]: t1.isoformat(" ")
Out[4]: '2020-05-08 22:16:39.287433'
```

もっとstrftimeを使うこともできます。

```
In [5]: t1.strftime("%Y/%m/%d %H:%M:%S.%f")
Out[5]: '2020/05/08 22:16:39.287433'
```

逆に文字列からdatetime に変換するには stfptime を使います。

```
In [6]: t2 = datetime.strptime("2020-01-02 20:03:12.345678", "%Y-%m-%d %H:%M:%S.%f")

In [7]: t2
Out[7]: datetime.datetime(2020, 1, 2, 20, 3, 12, 345678)
```

## 辞書を文字列(json)にする

json.dumpやjson.dumps を使うのですが、このとき引数にdefault に関数を設定し、その関数の中でdatetime を所望の文字列に変換することができます。以下、「[JSON エンコーダおよびデコーダ](https://docs.python.org/ja/3/library/json.html)」より：

>default を指定する場合は関数を指定して、この関数はそれ以外では直列化できないオブジェクトに対して呼び出されます。 その関数は、オブジェクトを JSON でエンコードできるバージョンにして返すか、さもなければ TypeError を送出しなければなりません。 指定しない場合は、 TypeError が送出されます。

TypeError を拾うのが正しいそうですね。

```python
import json
from datetime import datetime, date

def default(o):
    if hasattr(o, "isoformat"):
        return o.isoformat()
    else:
        return str(o)

# 辞書を作って現在時刻を値にする.
dict1 = {"time1": datetime.now(), "time2": datetime.now()}
print( "dict1={}".format(dict1))
print( "str(dict1)={}".format(str(dict1)) )
# 辞書を書き出す
s_dict1 = json.dumps(dict1, default=default)
print( "s_dict1={}".format(s_dict1) )
```

実行結果は以下の通りになります。

```
dict1={'time1': datetime.datetime(2020, 5, 8, 22, 57, 18, 564149), 'time2': datetime.datetime(2020, 5, 8, 22, 57, 18, 564163)}
str(dict1)={'time1': datetime.datetime(2020, 5, 8, 22, 57, 18, 564149), 'time2': datetime.datetime(2020, 5, 8, 22, 57, 18, 564163)}
s_dict1={"time1": "2020-05-08 22:57:18.564149", "time2": "2020-05-08 22:57:18.564163"}
```

きちんと文字列になっていますね。defaultの関数の実装ではisoformat を```hasattr```　かどうかで調べていますが、例えば```isinstance(o, (datetime, date)) ``` や ```type(o).__name__ == "datetime"``` なども使えます。 

## 文字列(json)から辞書

読むときは json.load を使います。ここではカスタマイズした読み取りは、object_hook で設定することができます。

> object_hook はオプションの関数で、任意のオブジェクトリテラルがデコードされた結果 (dict) に対し呼び出されます。 object_hook の返り値は dict の代わりに使われます。この機能は独自のデコーダ (例えば JSON-RPC クラスヒンティング) を実装するのに使えます。


```python
def object_hook(obj):
    new_dic = dict()
    for o in obj:
        try:
            new_dic[str(o)] = datetime.strptime(obj[o], '%Y-%m-%d %H:%M:%S.%f')
        except TypeError:
            new_dic[str(o)] = obj[o]
            pass
    return new_dic


s_dic = """{"time1": "2020-05-08 22:57:18.564149", "time2": "2020-05-08 22:57:18.564163"}"""
print( "s_dic={}".format(s_dic) )
dic = json.loads(s_dic, object_hook=object_hook)
print( "dic={}".format(dic) )
```

実行結果は以下の通り。

```
s_dic={"time1": "2020-05-08 22:57:18.564149", "time2": "2020-05-08 22:57:18.564163"}
dic={'time1': datetime.datetime(2020, 5, 8, 22, 57, 18, 564149), 'time2': datetime.datetime(2020, 5, 8, 22, 57, 18, 564163)}
```

きちんとdatetime.datetime となっている。

## その他

自分は以上で所望の実装を行うことができ、今日も一日生き延びることができました。

- json.JSONEncoder を継承したclass でdefault を実装し、このクラスをdumps の引数のcls で指定する方法も紹介されています。どちらが便利なのかな。
- ほぼいつもdatetimeを使うのですが、毎回これを実装するのも少しダルい。何か良い方法は無いかな。
 
(2020/05/08)


### 備考

- その後、これだと？今までfloat64 で読み込まれていたものが str になってしまった。困った。 (2020/05/12)