【Python】jsonモジュールの使い方

Pythonの標準ライブラリ、jsonモジュールの基本操作をまとめます。

:::note info
環境
Python 3.8.10
Ubuntu 20.04.3
:::


## 基本操作
### json.load
対象ファイルからjsonオブジェクトを受け取る。返り値は辞書型
```python
with open('jsonファイルのパス', 'r') as f:
    map_obj = json.load(f)
    print(type(map_obj) # <class 'dict'>
```

### json.loads
文字列からjsonオブジェクトを受け取る。返り値は辞書型
```python
str = '{"a": 1, "b": 2, "c": 3}'
map_obj = json.loads(str)
print(type(map_obj) 
# >> <class 'dict'>
```

### json.dump
辞書型の値をjsonに変換してファイル出力する。第一引数には変換するデータ、第2引数には、出力先のファイルを指定する。

```python
str = '{"a": 1, "b": 2, "c": 3}'
with open('jsonファイルのパス', 'w') as f:
    json.dump(str, f)
```

整形したい場合は、第三引数に指定する

```python
str = '{"a": 1, "b": 2, "c": 3}'
with open('jsonファイルのパス', 'w') as f:
    json.dump(str, f, indent=2)
```
### json.dumps
辞書型の値をjsonに変換しｍ、文字列として出力する。
```python
map = {a: 1, b: 2, c: 3}
print(json.dump(str)) # '{"a": 1, "b": 2, "c": 3}'
```



## エンコード、デコードのカスタマイズ
Pythonのjsonモジュールでエンコード・デコードできるデータ型は以下に限られている。

**JSON ⇔ Python**
- object ⇔dict
- array ⇔ list
- string ⇔ str
- number(int) ⇔ int
- number(real) ⇔ float
- true ⇔ True
- false ⇔ False
- nukk ⇔ None

そのため、他のデータ型を変換したい場合は、独自の関数を作成し、エンコーダーとデコーダーに渡してあげる必要がある。

## エンコードのカスタマイズ（datetime型）
```python
import json
from datetime import datetime

profiles = [
    {'Name': 'Tarou', 'Age': 15, 'Gender': 'male', 'birth_day': datetime(2000, 9, 11)},
    {'Name': 'Sayaka', 'Age': 20, 'Gender': 'female', 'birth_day': datetime(2000, 3, 11)},
]

def main():
    # dampsのclsにDatetimeJSONEncoderを渡す。
    # こうすることでデフォルトの方式ではなく
    # DatetimeJSONEncoderに定義したdefaultにしたがってエンコードされる。
    json_data = json.dumps(profiles, indent=4, cls=DatetimeJSONEncoder)
    print(json_data)

class DatetimeJSONEncoder(json.JSONEncoder):
    """JSONEncoderのdefaultを上書きする"""
    def default(self, o):
        if isinstance(o, datetime):
            # datetime型の時は、isoformatで文字列を変える。toordinalとかでも良い。
            return {'_type': 'datetime', 'value': o.isoformat()}
        else:
            # datetime型ではないときは、通常の方法の、defaultでエンコードする。
            # super()はjson.JSONEncoderと書いても良い。
            return super().default(o) 

if __name__ == '__main__':
    main()
```
### 出力結果
```json
[
    {
        "Name": "Tarou",
        "Age": 15,
        "Gender": "male",
        "birth_day": {
            "_type": "datetime",
            "value": "2000-09-11T00:00:00"
        }
    },
    {
        "Name": "Sayaka",
        "Age": 20,
        "Gender": "female",
        "birth_day": {
            "_type": "datetime",
            "value": "2000-03-11T00:00:00"
        }
    }
]
```

受け取ったdatetime型を返す時に、'_type'プロパティを与えているのは、複数のデータ型に対応するため、およびデコードするための前準備です。
```python
return {'_type': 'datetime', 'value': o.isoformat()}
```
というのも、、、エンコードするときは、データ型が分かります。
しかし、jsonからデコードするとき、jsonはただの文字列なので、データ型が判断できません。そのため、デコードするときに備えて、データの型を保存しておく必要があります。

## エンコードのカスタマイズ（クラス）
```python
import json

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

def main():
    bob = Person("Bob", 23)
    bob_json = json.dumps(bob, indent=4, cls=PersonEncoder)
    print(bob_json)

class PersonEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, Person):
            # classの情報を辞書型で取得するために、__dict__を仕様
            # あまり使わない方がよい、という記事もあり、ベストな書き方かは不明
            return {'_type': 'Person', 'value': o.__dict__}
        else:
            return json.JSONEncoder.default(o) 

if __name__ == '__main__':
    main()
```

### 出力結果
```json
{
    "_type": "Person",
    "value": {
        "name": "Bob",
        "age": 23
    }
}
```

datetime型の時とほとんど同じなので、特別な解説は不要かと思います。
## デコードのカスタマイズ（datetime型）
```python
import json
from datetime import datetime

profiles_json = '''[
    {
        "Name": "Tarou",
        "Age": 15,
        "Gender": "male",
        "birth_day": {
            "_type": "datetime",
            "value": "2000-09-11T00:00:00"
        }
    },
    {
        "Name": "Sayaka",
        "Age": 20,
        "Gender": "female",
        "birth_day": {
            "_type": "datetime",
            "value": "2000-03-11T00:00:00"
        }
    }
]'''

def main():
    profiles = json.loads(profiles_json, cls=DatetimeDecoder)
    print(profiles[0])
    print(f'birth_day: {profiles[0]["birth_day"]}')
    print(f'type of birth_day: {type(profiles[0]["birth_day"])}')


class DatetimeDecoder(json.JSONDecoder):
    """json.JSONDecorderのobject_hookを上書きする"""
    # __init__で何をやっているかはいまいち分かっていない。
    def __init__(self, *args, **kwargs):
        json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)

    def object_hook(self, o):
        if '_type' not in o:
            # _typeが定義されていない。つまり、jsonモジュールで変換がサポートされている型はそのまま返す。
            return o
        type = o['_type']
        if type == 'datetime':
            # _typeがdatetimeの場合、datetimeに変換して返す。。
            return datetime.fromisoformat(o['value'])

if __name__ == '__main__':
    main()
```
###  出力結果
```
{'Name': 'Tarou', 'Age': 15, 'Gender': 'male', 'birth_day': datetime.datetime(2000, 9, 11, 0, 0)}
birth_day: 2000-09-11 00:00:00
type of birth_day: <class 'datetime.datetime'>
```

datetime型でしっかり取得できているのが分かります。先ほども述べましたが、jsonは単なる文字列ではあるので、_typeのような形で型情報がなければ、このように正しいデータ型を取り出すのは難しいです。


## デコードのカスタマイズ（クラス）
```python
import json

def main():
    bob_json = '{"_type": "Person","value": {"name": "Bob","age": 23}}'
    person = json.loads(bob_json, cls=PersonDecoder)
    print(f'type: {type(person)}')
    print(f'name: {person.name}')
    print(f'age: {person.age}')

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

class PersonDecoder(json.JSONDecoder):
    """json.JSONDecorderのobject_hookを上書きする"""
    # __init__で何をやっているかはいまいち分かっていない。
    def __init__(self, *args, **kwargs):
        json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)

    def object_hook(self, o):
        if '_type' not in o:
            # _typeが定義されていない。つまり、jsonモジュールで変換がサポートされている型はそのまま返す。
            return o
        type = o['_type']
        if type == 'Person':
            # _typeがPersonクラスのものは、クラスのインスタンスとして返す。
            return Person(**o['value'])

if __name__ == '__main__':
    main()
```

### 出力結果
```
type: <class '__main__.Person'>
name: Bob
age: 23
```
datetime型の変換とほぼ一緒です。

## エンコードとデコート、datetime型とクラスすべて合わせる
分かりやすくするためにdatetimeとクラスを分けて書いていたのですが、一緒に書けます。

```python
class MyEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, date):
            return {'_type': 'date', 'value': o.isoformat()}
        elif isinstance(o, Person):
            return {'_type': 'Person', 'value': o.__dict__}
        else:
            return json.JSONEncoder.default(self, o) 

class MyDecoder(json.JSONDecoder):
    def __init__(self, *args, **kwargs):
        json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs)

    def object_hook(self, o):
        if '_type' not in o:
            return o
        type = o['_type']
        if type == 'date':
            return date.fromisoformat(o['value'])
        elif type == 'Person':
            return Person(**o['value'])
```
if文にほかの条件も書いてあげれば、jsonがデフォルトではサポートしていないデータ型も、柔軟に変換できるようになります。試してみてください。
他にももっといい書き方があると思いますので、分かる方がいたら教えてください！

## 参考
- [PythonでオブジェクトをJSONにEncode/Decodeする](https://zenn.dev/ku_mal/articles/b136b9394031a3)
- [PythonでJSONデータを操作する](https://www.codeflow.site/ja/article/python-json)
- [Python公式 JSON エンコーダおよびデコーダ](https://docs.python.org/ja/3.8/library/json.html)
- [Python でクラスオブジェクトをJSON形式にする・戻す](https://qiita.com/tanabe13f/items/a8a84368290d5b8da092)