Pythonで,自分で定義したクラスのオブジェクトを Json 形式にする (encode) 方法とJson形式から戻す (decode) 方法のメモ.
参考にしたページ:
- Pythonのjsonモジュールドキュメント
- How to use custom Python JSON serializers and deserializers to automatically roundtrip complex types
例
こんなクラスを例にする.
import json
from datetime import date
class MyA:
def __init__(self, i=0, d=date.today()):
self.i = i # int型
self.d = d # date型
Jsonデータ
まず始めに,オブジェクトをどのような形式でJsonデータにするのかを定めなくてはならない.MyA オブジェクトは,'i'
と'd'
をキーとした object にするのが自然に思える.また,date型のデータは,手っ取り早く toordinal メソッドで整数にしてしまおう.この考え方だと,たとえば
a = MyA(i=100, d=date(year=2345, month=6, day=12))
という a は,{"i": 100, "d": 856291}
というJsonデータに変換することになる.
この方式は,状況によっては,少し良くない.Json データを見た時に,これが MyA クラスのオブジェクトであることが直ちにはわからないので,デコードが簡単にいかない.そこで,定義を修正して,'_type'
というキーにクラス名を格納し,値は'value'
というキーに格納することにする.この方式だと,Jsonデータは次のようになる:
{ "_type": "MyA",
"value": {"i": 100,
"d": { "_type": "date",
"value": 856291}}}
MyA オブジェクトだけではなく,date オブジェクトについても _type
を使っているところに注意.以下,この方式を例に,エンコードとデコードを行ってみる.
エンコード
Python の json ライブラリの dump や dumps コマンドは,cls という引数をとる.ここに,自分で定義したエンコーダを指定することができる.JSONEncoder というのが普通使われるクラスなので,これをベースとして自分のエンコーダを作る.実際に必要なことは,default というメソッドの再定義である.
class MyEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, date):
return {'_type': 'date', 'value': o.toordinal()}
if isinstance(o, MyA):
return {'_type': 'MyA', 'value': o.__dict__} # (*)
return json.JSONEncoder.default(self, o)
ご覧のように,各クラスが直接見ているところだけ書けばよい.MyA オブジェクトを処理する (*) のところを見ると,_type
と value
をキーにする辞書を作っている.value
の値として,例えば上の a ならば,{"i": 100, "d": date(year=2345, month=6, day=12)}
という辞書を設定している.つまり,再帰処理はライブラリがよしなに行ってくれるのである.
次のように実行できる.
a = MyA(100, date(year=2345, month=6, day=12))
js = json.dumps(a, cls=MyEncoder)
print(js)
実行結果:
{"_type": "MyA", "value": {"i": 100, "d": {"_type": "date", "value": 856291}}}
デコード
こちらも,JSONDecoderというクラスをベースにデコーダを作る.今度は,親クラスであるJSONDecoderのオブジェクトが作成される際に,object_hook という変数に渡す関数の中で,必要なデコードが行われるように書く.以下のような具合である:
class MyDecoder(json.JSONDecoder):
def __init__(self, *args, **kwargs):
json.JSONDecoder.__init__(self, object_hook=self.object_hook,
*args, **kwargs)
def object_hook(self, o):
if '_type' not in o:
return o
type = o['_type']
if type == 'date':
return date.fromordinal(o['value'])
elif type == 'MyA':
return MyA(**o['value'])
object_hook の o としては,エンコードの時に作成したもの (MyEncoder::default の返り値) が渡ってくる.上の例だと,MyA オブジェクトや date オブジェクトについては '_type'
だの 'value'
だのをキーに持つ辞書が来る.それを適切なクラスのオブジェクトに作り替えるようにコードを書けば良い.
エンコードの時の実行に引き続いてやってみる:
b = json.loads(js, cls=MyDecoder)
print(b.i, b.d)
実行結果:
100 2345-06-12