PydanticでHTTPリクエストのJSONレスポンスに型情報を付与する

Requests や HTTPX などのライブラリを利用して HTTP リクエストを送り、JSON レスポンスをアプリケーション内で参照するといったケースはよくあると思います。

docs.python-requests.org

www.python-httpx.org

例えば HTTPX を利用して愚直に実装すると以下のように記述できます。

import httpx

r = httpx.get("https://jsonplaceholder.typicode.com/users/1")
r_json = r.json()

print(r_json.get("name"))  # => Leanne Graham

# OR

print(r_json["name"])

しかしネストした複雑な JSON を扱う場合は更に記述量が増えますし、エディタによる型補完もありません。何重にもネストしたフィールドの値を取り出す場合はさらに .get が増えて辛いですね。

import httpx

r = httpx.get("https://jsonplaceholder.typicode.com/users/1")
r_json = r.json()

print(r_json.get("company").get("name"))  # => Leanne Graham

# OR

print(r_json["company"]["name"])

今回は Pydantic を利用して JSON レスポンスに型情報を付与してあげて、エディタの型補完が可能となるようにしてみます。 サンプルとして JSONPlaceholder の /users リソースを利用します。

{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  }
}

jsonplaceholder.typicode.com

前提

  • MacOS Ventra
  • Python 3.11.3
  • Pydantic 2.8.2
  • httpx 0.27.0

クラスの定義

まずは下準備として BaseModel を継承したクラスを作成してあげます。

from pydantic import BaseModel


class Geo(BaseModel):
    lat: str
    lng: str


class Address(BaseModel):
    street: str
    suite: str
    city: str
    zipcode: str
    geo: Geo


class Company(BaseModel):
    name: str
    catchPhrase: str
    bs: str


class User(BaseModel):
    id: int
    name: str
    username: str
    email: str
    address: Address
    phone: str
    website: str
    company: Company

User インスタンスの生成・インスタンスの利用

/users/1 にリクエストすると、r_json の型は dict になります。そのため r_json の値をアンパックして User インスタンスを生成します。これで user の型は User となります。JSON 内の company フィールドの name フィールドを参照したい場合は、user.company.name でアクセスすることが可能で、かつエディタの型補完も効きます。

r = httpx.get("https://jsonplaceholder.typicode.com/users/1")
r_json = r.json()
user = User(**r_json)
print(user.company.name)  # => Romaguera-Crona

エディタ補完(1)

エディタ補完(2)

注意点

APIスキーマが変わってしまうと Pydantic で ValidationError が発生してしまうため注意が必要です。APIスキーマ変更とあわせてクラスの変更も忘れないようにしましょう。以下はリクエスト URL を /user/1 から /posts/1 に変更した場合のエラー内容です。

Traceback (most recent call last):
  File "/Users/enokawa/workspace/github.com/enokawa/python-sandbox/httpx-sample/httpx_sample/main.py", line 43, in <module>
    main()
  File "/Users/enokawa/workspace/github.com/enokawa/python-sandbox/httpx-sample/httpx_sample/main.py", line 38, in main
    user = User(**r_json)
           ^^^^^^^^^^^^^^
  File "/Users/enokawa/.anyenv/envs/pyenv/versions/3.11.3/lib/python3.11/site-packages/pydantic/main.py", line 171, in __init__
    self.__pydantic_validator__.validate_python(data, self_instance=self)
pydantic_core._pydantic_core.ValidationError: 7 validation errors for User

docs.pydantic.dev

おわりに

Pydantic で JSON レスポンスに型情報を付与する方法を紹介しました。その他の利用例として、boto3 で Secrets Manager の get_secret_value のレスポンスにも型情報を付与するといったことも可能になります。僕がいま関わっているプロジェクトでも多段 .get を利用している部分が多くあるため、少しずつこの方式に変えていきたいと企んでいます。

boto3.amazonaws.com

サンプルコード全文は以下を参照してください。

github.com