Pythonでjson文字列をdataclassに戻したい時のTips:field(init=False)を使っている場合

2024-05-27(Mon) Python

Pythonでjsonを扱うとき、同じ構造のdataclassで変換したいなーと思う時がありまして。ちょっと困るのがdataclassでfield(init=False)を使っている場合。

dataclassをjsonにする時はそれほど難しいくもなく、dataclass.asdictを使った上で、json.dump/json.dumpsでjson文字列にできます。

json文字列を適切なdataclassにしようとするとき、この操作が必要とわかりました。

  • 定義済みのdataclassにjson文字列->dictにして初期化する
  • 定義済みフィールドの中にfield(init=False)を使っていなければそのまま初期化できる
  • field(init=False)を使っているフィールドと、初期化時に指定できないので、フィールドの状態を見てjson側から情報を省く必要がある

※ここでのdataclassはあまり複雑な構造を対象にしていません。ネストされたdataclassとか、データ型がカスタム定義(クラス)ではないものです。

json文字列を指定したdataclassで変換する関数

from dataclasses import dataclass, field
import json


def convert_dataclass_to_jsonhash(json_hash_str: str, dataclass_):
    """
    json文字列を、同じ構造のdataclassの形式に変換する

    文字列はjsonのハッシュを想定して変換する。=> {"name":"hiro","age":"20"}
    dataclassはinit=Falseのフィールドは除外すること

    Args:
        json_hash_str (str): dataclass_と同じ構造のjson文字列。jsonのハッシュを想定
        dataclass_ ([type]): 変換先のdataclass

    Returns:
        dataclass_: 変換後のdataclass

    Raises:
        ValueError: json_strがjson文字列ではない場合
    """

    # json文字列をdictに変換する。
    converted_jsonhash = json.loads(json_hash_str)

    if not isinstance(converted_jsonhash, dict):
        raise ValueError("json_hash_str is not json hash string")

    # dataclassのフィールド名を取得する。init=Trueのフィールドのみを取得する
    dataclass_field_names = (
        dataclass_fieldname
        for dataclass_fieldname, dataclass_field in dataclass_.__dataclass_fields__.items()
        if dataclass_field.init is True
    )

    return dataclass_(
        **{
            dataclass_field_name: converted_jsonhash.get(dataclass_field_name)
            for dataclass_field_name in dataclass_field_names
        }
    )

# 以下から利用例

@dataclass
class PeopleData:
    name: str
    age: int
    timezone: str = field(init=False)


def main():
    conv_addressdata = convert_dataclass_to_jsonhash(
        '{"name": "hiro", "age": 20}', PeopleData
    )

    print(conv_addressdata)
    print(type(conv_addressdata))

    conv_addressdata.timezone = "Asia/Tokyo"

    print(conv_addressdata.name)
    print(conv_addressdata.age)
    print(conv_addressdata.timezone)


if __name__ == "__main__":
    main()

まとめ

このtipsは必要に迫られたので作ったものでした。field(init=False)指定のフィールドはオプション的な値として見ている状況で、dataclassで初期化するときに削っても問題ない場合になります。

field(init=False)指定でもjson側に値がある場合にdataclassに入れたい場合は、関数の中でdataclassを作って、除外したキーを保存しておいて、初期化した後に適時追加する作業が必要そうです。

あと、field(init=False)ではなく、default_factoryを使って、デフォルト値を入れるのも1つですが、そうするとフィールドを利用しているかしていないかをチェックする時にやや面倒かもなあと思ったりもします。

dataclass便利なんですが、データ構造としてフル活用する時に、外部からjsonなどでデータを受け付ける方法をどうするべきか、いいアイデアあったら教えて欲しい🤔

(後、コードはjsonのハッシュ構造とdataclassの構造に対して厳密に受け入れる検証はしていないので、ややネタなコードです。利用は自己責任で)