【Python】オブジェクトとJSONの変換ができるクラスを考える

ある同一のデータを別々のプログラミング言語で扱うといったケースは少なくないかと思います。例えば、API経由でのデータのやり取りや、データの加工や分析だけちょっとpythonを使いたいなどですかね。
そこで今回はJSON形式でのインターフェースとなるようなクラスをとりあえずpythonで用意できないかと考えました。

### 1. シリアライズ可能なフィールドを持つクラスの例
メインは`to_json`関数と`from_json`関数です
* `to_json` : 生成したオブジェクトからstringのjsonを生成します
* `from_json` : stringのjsonからオブジェクトを生成します
```py
import json

class PersonClass:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def to_json(self):
        return json.dumps(self.__dict__)

    @classmethod
    def from_json(cls, json_string):
        data = json.loads(json_string)
        return cls(**data)

    def __eq__(self, other):
        if isinstance(other, PersonClass):
            return self.__dict__ == other.__dict__
        return False

    def __hash__(self):
        return hash(tuple(sorted(self.__dict__.items())))

    def __str__(self):
        return f"PersonClass(name={self.name}, age={self.age})"

person_from = PersonClass("bob", 25)
print(person_from) # PersonClass(name=bob, age=25)
print(hash(person_from)) # 4914228952729239745

person_to = PersonClass.from_json(person_from.to_json())
print(person_to) # PersonClass(name=bob, age=25)
print(hash(person_to)) # 4914228952729239745

print(person_from == person_to) # True
```

### 2. シリアライズ不可のフィールドがある時
`PersonClass`がデフォルトでシリアライズ不可能なobject, Enum型のフィールドを持つ場合を考えます。
詳しくはこちらを参照 : https://qiita.com/vossibop/items/b902ceabf199272c9cf4

変更点としては以下です
* `CustomJSONEncoder`でシリアライズ時のオブジェクトの型変換などを定義
* `to_json`内の`json.dumps()`で上記encoderを使用する
* `from_json`内の`json.loads()`で得たdict型をそのままコンストラクタに渡すとオブジェクトのインスタンス生成がうまくできないので以下のようにして渡す
    * `return cls(data['name'], data['age'], Country(**data['country']))`
```py
import json
from datetime import date
from enum import Enum

class CustomJSONEncoder(json.JSONEncoder):
    def default(self, obj):
        # Enum -> str,int
        if isinstance(obj, Enum):
            return obj.value
        # datetime -> str
        if isinstance(obj, datetime):
            return obj.isoformat()
        # object -> dict
        elif isinstance(obj, object) and hasattr(obj, '__dict__'):
            return obj.__dict__
        # dict,str,int,... -> default       
        return super().default(obj)
      
class Language(Enum):
    EN = 1
    JA = 2
    FR = 3
    DE = 4
    IT = 5

class Country:
    def __init__(self, name: str, language: int):
        self.name = name
        self.language = Language(language)

    def to_json(self):
        return json.dumps(self.__dict__)

    @classmethod
    def from_json(cls, json_string):
        data = json.loads(json_string)
        return cls(**data)

    def __eq__(self, other):
        if isinstance(other, Country):
            return self.__dict__ == other.__dict__
        return False

    def __hash__(self):
        return hash(tuple(sorted(self.__dict__.items())))

    def __str__(self):
        return f"Country(name={self.name}, language={self.language})"


class PersonClass:
    def __init__(self, name: str, age: int, country: Country):
        self.name = name
        self.age = age
        self.country = country

    def to_json(self, encoder: JSONEncoder):
        return json.dumps(self, cls=encoder)

    @classmethod
    def from_json(cls, json_string):
        data = json.loads(json_string)
        return cls(data['name'], data['age'], Country(**data['country']))

    def __eq__(self, other):
        if isinstance(other, PersonClass):
            return self.__dict__ == other.__dict__
        return False

    def __hash__(self):
        return hash(tuple(sorted(self.__dict__.items())))

    def __str__(self):
        return f"PersonClass(name={self.name}, age={self.age}, country={self.country})"


country = Country("United States", 1)

person_from = PersonClass("bob", 25, country)
print(person_from) # PersonClass(name=bob, age=25, country=Country(name=United States, language=Language.EN))
print(hash(person_from)) # -7954654959397923626

person_to = PersonClass.from_json(person_from.to_json(CustomJSONEncoder))
print(person_to) # PersonClass(name=bob, age=25, country=Country(name=United States, language=Language.EN))
print(hash(person_to)) # -7954654959397923626

print(person_from == person_to) # True
```