カスタムオブジェクトの JSON エンコード/デコード

# 概要

カスタムクラスのインスタンス (オブジェクト) を含む複雑な構造を JSON エンコード/デコードする例について説明します。

言語は Python 3.8.1 です。

Python の場合、複雑なデータをシリアライズするには `pickle` を使うのが楽で良いのですが、Python 以外で読み書きしたい場合や、シリアライズしてもある程度の可読性がほしい場合は `JSON` を選ぶことが多いです。

ここで説明する方法以外にもやり方はあると思いますが、以下の点において、この方法が気に入っています。

1. Python 標準の [json モジュール](https://docs.python.org/ja/3/library/json.html)を使う。
1. エンコード/デコードのロジックを、クラスごとに分けられる。

# カスタムで複雑なデータの例

説明に使うものなので、そこまで複雑にはしませんが、以下の条件を満たすデータでやってみます。

1. カスタムクラスが複数入っていること。
1. カスタムオブジェクトの属性にもカスタムオブジェクトが入っていること。

```python
class Person:
    def __init__(self, name):
        self.name = name

class Robot:
    def __init__(self, name, creator=None):
        self.name = name
        self.creator = creator

alan = Person('Alan')
beetle = Robot('Beetle', creator=alan)
chappy = Robot('Chappy', creator=beetle)
```

`alan` は人間、`beetle` と `chappy` はロボットです。
以下、ロボットのデータをリストにして、そのリストをエンコード/デコードしてみたいと思います。

```python
robots = [beetle, chappy]
```

# エンコード

オブジェクトから JSON 文字列へシリアライズすることを**エンコード**といいます。
このリストには `Person` クラスと `Robot` クラスのオブジェクトが含まれているので、それらについてエンコードできるようにする必要があります。

## シンプルなエンコード

まずはシンプルな `Person` クラスのエンコードをしてみます。

### エンコードの仕様を決める

カスタムのオブジェクトをエンコードするには、どのようにエンコードするか決めなければなりません (仕様というやつです)。

ここでは、**クラス名** と **属性の内容** をそれぞれ name-value ペアにして出力することにします。
上記の `alan` の場合ですと、以下のような JSON 文字列になる想定です。

```json
{"class": "Person", "name": "Alan"}
```

### カスタムのエンコーダを作る

標準の [json.dumps 関数](https://docs.python.org/ja/3/library/json.html#json.dumps)に `cls` パラメタを指定することで、カスタムのエンコーダを使うことができます。
カスタムのエンコーダは、[json.JSONEncoder](https://docs.python.org/ja/3/library/json.html#json.JSONEncoder) を継承して、`default` メソッドをオーバーライドして作ります。
`default` メソッドの引数にオブジェクトが入ってくるので、`json.JSONEncoder` で扱える形 (ここでは `str` のみを含む `dict`) にして返せば OK です。

```python
import json

# Person オブジェクト用の、カスタムのエンコーダ
class PersonEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Person):
            return {'class': 'Person', 'name': obj.name}
        else:
            return super().default(obj)

print(json.dumps(alan, cls=PersonEncoder))

# 結果:
{"class": "Person", "name": "Alan"}
```

## 複雑なエンコード

次に `Robot` クラスのエンコーダを作りますが、これが複雑という訳ではありません。
「概要」で書いたように、**エンコードのロジックはクラスごとに分ける** からです。

```python
# Robot オブジェクト用の、カスタムのエンコーダ
class RobotEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Robot):
            return {'class': 'Robot', 'name': obj.name, 'creator': obj.creator}
        else:
            return super().default(obj)
```

ほぼ `PersonEncoder` と同じです。
ただし、先ほど `PersonEncoder` でやったようには行きません。返り値の中の `creator` を `json.JSONEncoder` で扱える形にしていないからです。
敢えてそのようにしてロジックを分けて、実際にエンコードする時には2つのエンコーダを合体させて使います。

### エンコーダを合体させる

エンコーダを合体させるには、多重継承を使って新しいクラスを作ります。

```python
# 2つのエンコーダを継承した、新しいエンコーダ
class XEncoder(PersonEncoder, RobotEncoder):
    pass

print(json.dumps(robots, cls=XEncoder))

# 結果:
[{"class": "Robot", "name": "Beetle", "creator": {"class": "Person", "name": "Alan"}}, {"class": "Robot", "name": "Chappy", "creator": {"class": "Robot", "name": "Beetle", "creator": {"class": "Person", "name": "Alan"}}}]
```

<details>
<summary>結果を整形した場合 (クリックで表示)。</summary><div>

```python
print(json.dumps(robots, cls=XEncoder, indent=4))

# 結果:
[
    {
        "class": "Robot",
        "name": "Beetle",
        "creator": {
            "class": "Person",
            "name": "Alan"
        }
    },
    {
        "class": "Robot",
        "name": "Chappy",
        "creator": {
            "class": "Robot",
            "name": "Beetle",
            "creator": {
                "class": "Person",
                "name": "Alan"
            }
        }
    }
]
```
</div>
</details>

`json.dumps` 関数にエンコーダのクラスを1個しか指定できないのでこのような方法になりますが、これでオブジェクトの種類が増えた場合でも拡張が可能です。

### (補足) 多重継承による動作について

上記の `XEncoder` を作ることでなぜ上手く動作するのか、簡単に説明します。

[Python のクラスの多重継承](https://docs.python.org/ja/3/tutorial/classes.html#multiple-inheritance)では、継承順に属性を参照に行きます。
`XEncoder` の `default` メソッドを呼び出すと、まず先に継承している `PersonEncoder` の `default` メソッドを見に行きます。

`PersonEncoder.default` メソッドでは、`obj` が `Person` オブジェクトなら自分で `dict` を返し、そうでないならスーパーメソッドを呼ぶようになっています。

この場合のスーパーメソッドは、**`json.JSONEncoder.default` ではなく `RobotEncoder.default`** になります。
これが Python の多重継承の動きです。

`RobotEncoder.default` がスーパーメソッドを呼べば、それ以上は多重継承していないので、本来のスーパークラスである `json.JSONEncoder` へと処理が委ねられます。

`default` メソッドがどのように再帰的に呼ばれるのかは調べていませんが、`if` 文でクラス判定をしている限りは、継承順が逆になっても同じ結果が得られるようです。

# デコード

エンコードとは逆に、JSON 文字列からオブジェクトへデシリアライズすることを**デコード**といいます。
[json.loads](https://docs.python.org/ja/3/library/json.html#json.loads) メソッドに `object_hook` パラメタを渡すことで、デコード後のオブジェクトにカスタムの処理を加えることができます。

## シンプルな object_hook の例

まずは `Person` クラスのオブジェクトだけをエンコードし、それをデコードする例を見てみます。
`object_hook` として渡す関数はデコード済みのオブジェクト (`dict` 等) を受け取るので、'class' の値が 'Person' である `dict` だった場合の処理を書きます。

```python
# Person オブジェクト用のフック関数
def person_hook(obj):
    if type(obj) == dict and obj.get('class') == 'Person':
        return Person(obj['name'])
    else:
        return obj

# JSON 文字列にエンコードする
alan_encoded = json.dumps(alan, cls=PersonEncoder)
# JSON 文字列からデコードする
alan_decoded = json.loads(alan_encoded, object_hook=person_hook)

print(alan_decoded)
print(alan_decoded.__class__.__name__, vars(alan_decoded))

# 結果:
<__main__.Person object at 0x0000027F67919AF0>
Person {'name': 'Alan'}
```

## object_hook を合体させる

次に、`Robot` クラスのための `object_hook` を作って、2つを合体させた新しい関数を作ります。

```python
# Robot オブジェクト用のフック関数
def robot_hook(obj):
    if type(obj) == dict and obj.get('class') == 'Robot':
        return Robot(obj['name'], creator=obj['creator'])
    else:
        return obj

# 2つのフック関数を呼び出す、新しいフック関数
def x_hook(obj):
    return person_hook(robot_hook(obj))
```

合体させた関数 `x_hook` は以下のようにも書けます。少し長くなりますが、hook を増やしやすいです (上の例とは hook の適用順が変わるが、問題ない)。

```python
def x_hook(obj):
    hooks = [person_hook, robot_hook]
    for hook in hooks:
        obj = hook(obj)
    return obj
```

これを使って、上の方で作ったロボットのリストをエンコード/デコードしてみます。

```python
# JSON 文字列にエンコードする
robots_encoded = json.dumps(robots, cls=XEncoder)
# JSON 文字列からデコードする
robots_decoded = json.loads(robots_encoded, object_hook=x_hook)

for robot in robots_decoded:
    print(robot)
    print(robot.__class__.__name__, vars(robot))

# 結果:
<__main__.Robot object at 0x0000027F67919A30>
Robot {'name': 'Beetle', 'creator': <__main__.Person object at 0x0000027F67919B50>}
<__main__.Robot object at 0x0000027F67919E50>
Robot {'name': 'Chappy', 'creator': <__main__.Robot object at 0x0000027F679199D0>}
```

エンコードの時と同様に (おそらく内側から再帰的にデコードされるので)、hook を適用する順番を変えても結果は変わりませんでした。

## (補足) エンコードも同じ方法でカスタマイズできる

実は、エンコード側も同じように関数を与える形でカスタマイズする事ができます。
逆に、デコード側をデコーダのサブクラスを作る形にしようとすると、もっとややこしくなります。

カスタムのエンコードのロジックを合体させる時に、シンプルに多重継承のみで書きたい場合はサブクラスを作る方法を、デコード側のスタイルに合わせたい場合は関数を与える方法を選ぶと良いでしょう。

### エンコード側を関数を与える形でカスタマイズした例

```python
def person_default(obj):
    if isinstance(obj, Person):
        return {'class': 'Person', 'name': obj.name}
    else:
        return obj

def robot_default(obj):
    if isinstance(obj, Robot):
        return {'class': 'Robot', 'name': obj.name, 'creator': obj.creator}
    else:
        return obj

def x_default(obj):
    defaults = [person_default, robot_default]
    for default in defaults:
        obj = default(obj)
    return obj

print(json.dumps(robots, default=x_default))

# 結果:
[{"class": "Robot", "name": "Beetle", "creator": {"class": "Person", "name": "Alan"}}, {"class": "Robot", "name": "Chappy", "creator": {"class": "Robot", "name": "Beetle", "creator": {"class": "Person", "name": "Alan"}}}]
```

# 課題

デコードの方に少し課題があります。
上記の例でいうと、最初にデコードしたロボット 'Beetle' と、'Chappy' の `creator` である 'Beetle' は、元々は同じオブジェクトでした。
また、それらの 'Beelte' の `creator` である 'Alan' も同じオブジェクトでした。

上記のデコード方法では、「名前が同じだからすでに作ったオブジェクトを使い回す」という事はしていないので、完全にはエンコード前の状況を再現していません。
それをしたい場合は、`Person` クラスや `Robot` クラスの方にその仕組みを作って、`object_hook` からは名前を指定するだけで適切なオブジェクトを受け取れるようにすれば良いでしょう。