dataclasses-jsonでPythonでも手軽にキャメルケースのJSONを扱う

# はじめに
n番煎じ感が否めませんが、Pythonでjsonを扱う際に`dataclass`×`dataclasses-json`の組み合わせが便利だったので紹介します。
## 結局何が便利？
- JSONとの変換がto_json()メソッドとfrom_json()メソッドだけで行える
    - to_dict()メソッドとfrom_dict()メソッドにより辞書型に変換することも可能です
- ネストされたクラスでも**下の階層まで各クラスに展開**してくれる
- スネークケースとキャメルケース（やパスカルケースなど）を**自動的に変換**してくれる
    - Pythonはスネークケース、jsonはキャメルケース、というパターンが多いため、この機能によって命名規則に困らなくなります

<details>
<summary>余談</summary>
後から知ったのですが、<b>Pydantic</b>でも同様の機能が提供されているようです
</details>

# 準備
## 環境
今回テストした環境は以下の通りです
- Python 3.12.0
- dataclasses-json 0.6.7

dataclasses-jsonは`pip`でインストールすることが可能です
```console
$ pip install dataclasses-json
```
:::note warn
dataclass自体に関しては既に多くの記事で詳しく紹介されているため、本記事では特に取り扱いません
:::

# 実践
## クラスの用意
今回は以下のようなネストされたFamilyクラスを使用して具体的に見ていきます

変数名は**スネークケース**で宣言されていますが、クラスの定義の前に`@dataclass_json(letter_case=LetterCase.CAMEL)`デコレータがついています

このデコレータの引数で記法を指定することによって自動的にスネークケースとキャメルケースを変換してくれます
```python
from dataclasses import dataclass
from dataclasses_json import dataclass_json, LetterCase

@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Address:
    postal_code: str
    country: str
    city: str
    street: str

@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Person:
    first_name: str
    last_name: str
    age: int
    address: Address

@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Family:
    members: list[Person]
```
### デコレータのLetterCaseについて
CAMELケース以外にも、SNAKE、KEBAB、PASCALが用意されているようです
※何も指定しなかった場合は変数名に使用しているケースがそのまま適用されます
## to_json
オブジェクトをJSONに変換するにはdataclass_jsonで追加される.to_json()メソッドを使用します
```python
family_obj = Family(
    members=[
        Person(
            first_name='hoge',
            last_name='fuga',
            age=20,
            address=Address(
                postal_code='100-0000',
                country='Japan',
                city='Tokyo',
                street='hoge street'
            )
        ),
        Person(
            first_name='foo',
            last_name='bar',
            age=30,
            address=Address(
                postal_code='200-0000',
                country='Japan',
                city='Osaka',
                street='foo street'
            )
        )
    ]
)
json_str = family_obj.to_json(indent=4)
print(json_str)
# output
# {
#     "members": [
#         {
#             "firstName": "hoge",
#             "lastName": "fuga",
#             "age": 20,
#             "address": {
#                 "postalCode": "100-0000",
#                 "country": "Japan",
#                 "city": "Tokyo",
#                 "street": "hoge street"
#             }
#         },
#         {
#             "firstName": "foo",
#             "lastName": "bar",
#             "age": 30,
#             "address": {
#                 "postalCode": "200-0000",
#                 "country": "Japan",
#                 "city": "Osaka",
#                 "street": "foo street"
#             }
#         }
#     ]
# }
```
to_jsonの引数でindentを4に指定することで整形して出力してくれます
（この辺りはjsonモジュールのdumpsと同様ですね）

注目すべきは出力されたJSON文字列のキー名が**キャメルケース**になっていることです
先ほど、クラスの定義時にデコレータでキャメルケースを指定しておいたことで、自動的に変換された状態でJSON文字列になってくれています

これでデフォルトの命名規則がキャメルケースのTypeScriptなどとJSONでやり取りする時も困りませんね
## from_json
逆にJSON文字列からオブジェクトに変換する際はfrom_dict()メソッドを使用します
```python
new_json_str = '''
{
    "members": [
        {
            "firstName": "hogehoge",
            "lastName": "fugafuga",
            "age": 30,
            "address": {
                "postalCode": "300-0000",
                "country": "Japan",
                "city": "Fukuoka",
                "street": "hogehoge street"
            }
        },
        {
            "firstName": "foobar",
            "lastName": "barfoo",
            "age": 40,
            "address": {
                "postalCode": "400-0000",
                "country": "Japan",
                "city": "Nagoya",
                "street": "foobar street"
            }
        }
    ]
}
'''
new_family_obj = Family.from_json(new_json_str)
print(new_family_obj)
# output
# Family(
#     members=[
#         Person(
#             first_name='hogehoge', last_name='fugafuga', age=30,
#             address=Address(postal_code='300-0000', country='Japan', city='Fukuoka', street='hogehoge street')
#         ),
#         Person(
#             first_name='foobar', last_name='barfoo', age=40,
#             address=Address(postal_code='400-0000', country='Japan', city='Nagoya', street='foobar street')
#         )
#     ]
# )
```
新しくnew_json_strを用意し、**クラス名.from_json**の形で呼び出します

今回はJSON文字列⇒オブジェクトなので、キャメルケース⇒スネークケースに自動的に変換されていますね
ちなみに、JSON文字列内のキーにスネークケースが含まれていても問題なく変換可能でした

## to_dict
ここまではオブジェクトとJSONとの変換でしたが、dataclasses-jsonでは辞書型との変換もサポートされています
まずはオブジェクトを辞書型に変換するto_dict()メソッドです
```python
print(family_obj)
# output
# Family(
#   members=[
#       Person(
#           first_name='hoge', last_name='fuga', age=20,
#           address=Address(postal_code='100-0000', country='Japan', city='Tokyo', street='hoge street')
#       ),
#       Person(
#           first_name='foo', last_name='bar', age=30,
#           address=Address(postal_code='200-0000', country='Japan', city='Osaka', street='foo street')
#       )
#   ]
# )
family_dict = family_obj.to_dict()
print(family_dict)
# output
# {
#     'members': [
#         {
#             'firstName': 'hoge', 'lastName': 'fuga', 'age': 20,
#             'address': {'postalCode': '100-0000', 'country': 'Japan', 'city': 'Tokyo', 'street': 'hoge street'}
#         },
#         {
#             'firstName': 'foo', 'lastName': 'bar', 'age': 30,
#             'address': {'postalCode': '200-0000', 'country': 'Japan', 'city': 'Osaka', 'street': 'foo street'}
#         }
#     ]
# }
```

## from_dict
from_dict()メソッドで辞書型からオブジェクトに変換できます
```python
new_family_dict = {
    'members': [
        {
            'firstName': 'hogehoge', 'lastName': 'fugafuga', 'age': 30,
            'address': {'postalCode': '300-0000', 'country': 'Japan', 'city': 'Fukuoka', 'street': 'hogehoge street'}
        },
        {
            'firstName': 'foobar', 'lastName': 'barfoo', 'age': 40,
            'address': {'postalCode': '400-0000', 'country': 'Japan', 'city': 'Nagoya', 'street': 'foobar street'}
        }
    ]
}
new_family_obj = Family.from_dict(new_family_dict)
print(new_family_obj)
# output
# Family(
#     members=[
#         Person(
#             first_name='hogehoge', last_name='fugafuga', age=30,
#             address=Address(postal_code='300-0000', country='Japan', city='Fukuoka', street='hogehoge street')
#         ),
#         Person(
#             first_name='foobar', last_name='barfoo', age=40,
#             address=Address(postal_code='400-0000', country='Japan', city='Nagoya', street='foobar street')
#         )
#     ]
# )
```
from_dict()メソッドでもfrom_json()メソッドと同様に、元の辞書型がキャメルケースではなくスネークケースでも問題なく変換できました

# まとめ
- dataclass定義時にdataclass_jsonデコレータを付与する
    - 引数のletter_caseで変換時の記法を指定できる
- **オブジェクトから**変換
    - to_json()メソッドやto_dict()メソッド
- **オブジェクトに**変換
    - from_json()メソッドやfrom_dict()メソッド

# 参考
- [dataclasses-json API](https://lidatong.github.io/dataclasses-json/reference/dataclasses_json/api/#dataclass_json)
- [dataclass_jsonを利用したJSONとPythonオブジェクトの相互変換](https://gri.jp/media/entry/8261)
- [Dataclassをjson形式でシリアライズ](https://horomary.hatenablog.com/entry/2019/11/24/033557)