JSONDecoder を継承してJSON→任意クラスへの変換

任意クラスをネストした構造への JSON読込みオブジェクト変換は、json.load() の結果のdict:辞書
丹念にパースすればできますが、膨大なコードを書きたくありません。
json.load() object_hook でなんとかならないのかと悩みましたが、日付時刻型などは対応できても
任意クラスの自由な構造を許可する構造を汎用的に書くには無理があります。
日付時刻型だけをobject_hook でなんとかするとして、
任意クラスの自由な構造は、やはり JSONDecoder を継承して最後のdict:辞書を解析するしかないようです。
object_hook で、日付時刻型も対処したJSONDecoder継承にすれば、良いようです。

JSONファイル:item.json

{
  "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)