PythonでhttpでAPIの送受信を行うとどうしてもJSON↔︎オブジェクトの変換が必要になってくる。オブジェクトへの変換は何通りか方法があるが、それぞれのPros/Consを挙げる。
基本的にはdataclasses-jsonを利用するのが良い。書き捨てのコードの場合は、他の方法が無駄がなく良い。
- 方法1: 力技
- 方法2: json.loads()のobject_hookでコンストラクタを利用する
- 方法3: json.loads()のobject_hookでSimpleNamespaceを利用する
- 方法4: dataclasses-jsonを利用する
- 終わりに
方法1: 力技
1つ目は、requests.get()のレスポンスをdictで取得した際に、力技で各フィールドに直接アクセスする方法。単純でわかりやすいが、複雑なJSONには向かない。
from dataclasses import dataclass from datetime import date, datetime @dataclass class Foo(): foo_str: str foo_int: int foo_date: date foo_list: list[str] # 例えば下記実施後 # response = requests.get("https://...") # input_dict = response.json() input_dict = {"foo_str": "foofoo", "foo_int": 10, "foo_date": "2024-12-31","foo_list":["foo1", "foo2", "foo3"]} try: f = Foo( foo_str=input_dict["foo_str"], foo_int=input_dict["foo_int"], foo_date=datetime.strptime(input_dict["foo_date"], "%Y-%m-%d"), foo_list=input_dict["foo_list"], ) print(f) # Foo(foo_str='foofoo', foo_int=10, foo_date=datetime.datetime(2024, 12, 31, 0, 0), foo_list=['foo1', 'foo2', 'foo3']) except Exception as e: print("because: ", e)
Pros
- 単純で初心者にもわかりやすい。
- フィールドが少なく、コードを書き捨てる場合は向いている。
Cons
- 変換するフィールドが多い、ネストが深いと書く量が増える。
datetimeなどJSONには無い型は変換を実装する必要がある。
方法2: json.loads()のobject_hookでコンストラクタを利用する
2つ目は、json.loads()のobject_hookを利用する方法。JSONのフィールドが増えてもコードの増加量は方法1よりは低い。一方でJSONに存在しない型への変換はobject_hookに指定する関数内で実装する必要がある。
import re import json from dataclasses import dataclass from datetime import date, datetime r = re.compile(r"\d{4}-\d{2}-\d{2}") @dataclass class Foo: foo_str: str foo_int: int foo_date: date foo_list: list[str] def hook(s): d = s.get("foo_date") if d is not None and r.match(d): s["foo_date"] = datetime.strptime(d, "%Y-%m-%d") return Foo(**s) # 例えば下記実施後 # response = requests.get("https://...") # input_str = response.text したと仮定 input_str = '{"foo_str": "foofoo", "foo_int": 10, "foo_date": "2024-12-31", "foo_list":["foo1", "foo2", "foo3"]}' try: f = json.loads(input_str, object_hook=hook) print(f) # Foo(foo_str='foofoo', foo_int=10, foo_date=datetime.datetime(2024, 12, 31, 0, 0), foo_list=['foo1', 'foo2', 'foo3']) print(type(f.foo_date)) # <class 'datetime.datetime'> except Exception as e: print("because: ", e)
Pros
- 変換するフィールドが多くなっても、方法1の力技よりも書く量が少ない。
- 変換したい型が
strやint、boolだけなら、hook関数は作成せず、object_hook=lambda d: Foo(**d)とさらに簡潔に書ける。
Cons
datetimeなどstrやint、bool以外の型に変換したい場合はhookでフィールドを指定して変換する必要がある。datetimeの場合はフォーマットが決まっている必要がある。
方法3: json.loads()のobject_hookでSimpleNamespaceを利用する
方法3は、SimpleNamespaceを利用して方法2よりもコード量を減らした書き方。方法2と同じデメリットはあるが、フィールドやネストが増えてもコードを修正する必要がない(JSONに存在する型以外へ変換する必要がない場合)。
import json import re from datetime import datetime from types import SimpleNamespace r = re.compile(r"\d{4}-\d{2}-\d{2}") def hook(s): d = s.get("foo_date") if d is not None and r.match(d): s["foo_date"] = datetime.strptime(d, "%Y-%m-%d") return SimpleNamespace(**s) # 例えば下記実施後 # response = requests.get("https://...") # input_str = response.text したと仮定 input_str = '{"foo_str": "foofoo", "foo_int": 10, "foo_date": "2024-12-31", "foo_list":["foo1", "foo2", "foo3"]}' try: f = json.loads(input_str, object_hook=hook) print(f) # namespace(foo_str='foofoo', foo_int=10, foo_date=datetime.datetime(2024, 12, 31, 0, 0), foo_list=['foo1', 'foo2', 'foo3']) print(type(f.foo_date)) # <class 'datetime.datetime'> except Exception as e: print("because: ", e)
Pros
dataclassを定義する必要がないため、さらに書く量を減らすことができる。
Cons
dataclassが無いために、何の変数があるのか不明。- オブジェクト変数へのアクセスにIDEによる補完が効かない。
方法4: dataclasses-jsonを利用する
最後はdataclasses-jsonを利用する方法。dataclassの定義は必要だが、定義をしっかりと書けば変換される。コード量は方法2と大して変わらないが、JSONがネストしていても変換される。またJSONのフィールドが特定の条件下で無い場合も、Optionalで型定義することでデフォルト値を埋められる。
from dataclasses import dataclass, field from datetime import date from marshmallow import fields, ValidationError from dataclasses_json import dataclass_json, LetterCase, config from typing_extensions import Optional @dataclass_json(letter_case=LetterCase.SNAKE) @dataclass class Bar: bar_str: str bar_date: date = field(metadata=config(mm_field=fields.Date())) bar_none: Optional[str] = None @dataclass_json(letter_case=LetterCase.SNAKE) @dataclass class Foo: bar: Bar foo_str: str foo_int: int foo_date: date = field(metadata=config(mm_field=fields.Date())) foo_list: list[str] foo_none: Optional[str] = None # 例えば下記実施後 # response = requests.get("https://...") # input_str = response.text したと仮定 input_str = '{"bar":{"bar_str":"barbar", "bar_date": "2025-01-01", "bar_none":"baz"}, "foo_str": "foofoo", "foo_int": 10, "foo_date": "2024-12-31", "foo_list":["bar1", "bar2", "bar3"]}' try: f = Foo.schema().loads(input_str) print(f) # Foo(bar=Bar(bar_str='barbar', bar_date=datetime.date(2025, 1, 1), bar_none='baz'), foo_str='foofoo', foo_int=10, foo_date=datetime.date(2024, 12, 31), foo_list=['bar1', 'bar2', 'bar3'], foo_none=None) print(type(f.foo_date)) # <class 'datetime.datetime'> print(type(f.bar.bar_date)) # <class 'datetime.datetime'> except ValidationError as e: print("because: ", e)
Pros
dataclassで定義した型に、ネストも含めそのまま変換できる。- Optionalな変数なのかが一目でわかる。
Cons
SimpleNamespaceと比べると書く量は多いので、単純なオブジェクトや書き捨てのコードには向かない。
終わりに
JSONからPythonのオブジェクトに変換する4つの方法とそれぞれのPros/Consを挙げた。いずれの方法も使い所はあるが、プロダクションコードを書くのであればdataclasses-jsonが最適だと考えている。
もし挙げた以外の方法で、もっと良い方法があれば教えていただきたいです。