任意クラスをネストした構造への JSON読込みオブジェクト変換は、json.load() の結果のdict:辞書を
丹念にパースすればできますが、膨大なコードを書きたくありません。
json.load() の object_hook でなんとかならないのかと悩みましたが、日付時刻型などは対応できても
任意クラスの自由な構造を許可する構造を汎用的に書くには無理があります。
日付時刻型だけをobject_hook でなんとかするとして、
任意クラスの自由な構造は、やはり JSONDecoder を継承して最後のdict:辞書を解析するしかないようです。
object_hook で、日付時刻型も対処したJSONDecoder継承にすれば、良いようです。
{ "name": "Apple", "price": 1200, "flag": false, "mount": [ 120, 140 ], "current": 1.02, "latest": "2020-12-11 09:46:26", "history": [ { "version": 1.0, "updated": "2020-12-01 10:00:00" }, { "version": 1.01, "updated": "2020-12-09 22:30:52" } ] }
このJSONを読込んで、構成するクラス Item と ItemHistory
from datetime import datetime from pytz import timezone from itemhistory import ItemHistory class Item(): def __init__(self): pass @property def _name(self): return self.name @_name.setter def _name(self, value): self.name = value @property def _price(self): return self.price @_price.setter def _price(self, value): self.price = value @property def _flag(self): return self.flag @_flag.setter def _flag(self, value): self.flag = value @property def _mount(self): return self.mount def addMount(self, value): self.mount.append(value) @property def _current(self): return self.current @_current.setter def _current(self, value): self.current = value @property def _latest(self): return self.latest @_latest.setter def _latest(self, value): self.latest = value @property def _history(self): return self.history def addHistory(self): h = ItemHistory() h.version = self.current h.updated = self.latest self.history.append(h) self.current = self.current + 0.01 self.latest = datetime.now(tz=timezone('Asia/Tokyo'))
Item の historyが、ItemHistoryのリストになります
class ItemHistory(): def __init__(self): self.version = None from datetime import datetime from pytz import timezone self.updated = datetime.now(tz=timezone('Asia/Tokyo')) @property def _version(self): return self.version @_version.setter def _version(self, value): self.version = value @property def _updated(self): return self.updated @_updated.setter def _updated(self, value): self.updated = value
customs.py:JSONDecoder を継承したクラス、CustomDecoder を定義します。
# -*- coding: UTF-8 -*- import re from json import JSONDecoder, JSONDecodeError from datetime import datetime, date class CustomDecoder(JSONDecoder): FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) def __init__(self, decode_hook = None): JSONDecoder.__init__(self, object_hook=self.object_hook) self.decode_hook = decode_hook self._rt = [ ( re.compile( '^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01]) (0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[0-5][0-9]):(0[0-9]|[0-5][0-9])$'), '%Y-%m-%d %H:%M:%S' ), ( re.compile( '^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[0-5][0-9]):(0[0-9]|[0-5][0-9])$'), '%Y-%m-%dT%H:%M:%S' ), ( re.compile( '^\d{4}/(0[1-9]|1[012])/(0[1-9]|[12][0-9]|3[01]) (0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[0-5][0-9]):(0[0-9]|[0-5][0-9])$'), '%Y/%m/%d %H:%M:%S' ), ( re.compile('^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$'), '%Y-%m-%d' ), ( re.compile('^\d{4}/(0[1-9]|1[012])/(0[1-9]|[12][0-9]|3[01])$'), '%Y/%m/%d' ), ] # Object hook def object_hook(self, obj): if isinstance(obj, dict): for k, v in obj.items(): obj[k] = self.object_hook(v) elif isinstance(obj, list): for i, e in enumerate(obj): obj[i] = self.object_hook(e) elif isinstance(obj, str): try: dlst = [datetime.strptime(obj, rtt[1]) for rtt in self._rt if rtt[0].match(obj)] if len(dlst) > 0: obj = dlst[0] except (ValueError, AttributeError): pass return obj # decode を override して、最後に decode_hook を実行する def decode(self, s, _w=WHITESPACE.match): obj, end = self.raw_decode(s, idx=_w(s, 0).end()) end = _w(s, end).end() if end != len(s): raise JSONDecodeError("Extra data", s, end) return self.decode_hook(obj)
dict:(辞書)から任意のクラスオブジェクトに変換する為のメソッド
=decode_hook に指定するメソッドを定義します。
from item import Item def item_parser(obj): item = Item() for k,v in obj.items(): if k=='history': if isinstance(obj[k], list): res = list() for _o in obj[k]: h = ItemHistory() for _k,_v in _o.items(): setattr(h, _k, _v) res.append(h) else: res = None setattr(item, k, res) else: setattr(item, k, v) return item
'history' キー名で読込んだものをリストとして、ItemHistory を生成するようにしてます。
CustomDecoder をcls= で指定、decode_hook をパラメータ追加で呼出し実行します。
import codecs import json from customs import CustomDecoder with codecs.open('item.json', 'r', 'utf-8') as f: i = json.load(f, cls=CustomDecoder, decode_hook=item_parser)
結果をtype() で調べると
print(type(i)) # <class 'item.Item'> print(i.name) print(i.price) print(i.current) print(i.flag) print(i.mount) print(i.latest) print(type(i.latest)) print(i.history) for h in i.history: print(type(h)) print(h.version) print(h.updated) print(type(h.updated))
decode_hook に指定するメソッドは、Item クラスのメソッドとして
定義しても良いと思います。
その場合呼出しは、Item インスタンスからの呼びだしではなくて
staticメソッドとしての呼び出しです。
i = json.load(f, cls=CustomDecoder, decode_hook=Item.item_parser)