の応答を辞書のまま扱うのは手軽です。items の 0 番目の price を鍵でたどれば値が取れます。動いている間はこれで十分に見えます。
問題は、配信元が形を変えたときです。項目名が price から unit_price に変わった、数値だった値が文字列になった、あった項目が消えた。辞書のまま扱っていると、これらは取得の時点では何も起きません。としては正しい形なので、読み込みは成功してしまいます。
項目名が変わった場合は、まだ気づきやすいほうです。存在しない鍵を引こうとして例外になるからです。本当に厄介なのは、型だけが変わった場合です。数値が文字列になっても鍵は引けるので、値が取れてしまいます。そのまま計算に回って、意味のない結果が出ます。
実際に何かが起きるのは、ずっと下流です。集計しようとして型が合わない、と数十行先で例外が出る。あるいは例外すら出ず、文字列が結合されて意味のない値ができる。原因の場所と、症状の出る場所が離れるのが、この作りの一番の問題です。
受け取った時点で形を確かめる
対策は、応答を受け取った直後に「こういう形のはずだ」と宣言しておき、実際に届いたものがその形になっているかどうかを、その場で確かめることです。宣言そのものが、応答の仕様を書き残したものにもなります。 はこれを短く書けるようにしてくれます。形が違えば、その場で分かりやすい例外が出ます。
重要なのは、失敗する位置が取得の直後に移ることです。「配信元の応答が想定と違う」という、原因そのものを指す形で止まります。下流で意味不明な型エラーを追いかける必要がなくなります。
この位置の移動には、副次的な効果もあります。エラーメッセージが読める形になることです。どの項目が、何を期待していて、実際は何だったのか。これが例外に含まれるので、配信元へ問い合わせるときの材料にもそのまま使えます。
from datetime import date
from pydantic import BaseModel, Field, ValidationError
class Item(BaseModel): id: str name: str | None = None price: float # 文字列で来ても数値に直される。直せなければ例外 updated_at: date
class ItemsResponse(BaseModel): items: list[Item] total: int = Field(ge=0) # 負の総数はありえない next_cursor: str | None = None
def parse_response(payload: dict) -> ItemsResponse: try: return ItemsResponse.model_validate(payload) except ValidationError as e: # どの項目が、どう違ったのかが例外に含まれる raise RuntimeError(f"応答の形が想定と違う: {e}") from eprice: float と宣言しておけば、「1200」という文字列 のような文字列は数値に直されます。しかし 「1,200」のように桁区切りが入った文字列 のように桁区切りが入っていると直せず、例外になります。直せない値が来たことを知れるのが利点で、黙って落とさないことに意味があります。
厳しくしすぎない
全項目を必須にすると、配信元が項目を1つ足しただけで壊れる、ということはありません。逆に、使わない項目まで必須にすると、そちらが欠けただけで止まります。
宣言するのは、自分が実際に使う項目だけにしてください。使わない項目は無視されます。この線引きをしておくと、配信元の変更のうち自分に関係あるものだけで止まるようになります。
- 使う項目は必須にする — 欠けたら気づきたいので
- 欠けうる項目は
| Noneにする — 実際に空で来ることがあるなら - 使わない項目は宣言しない — 増えても減っても影響を受けない
止めるか、記録して進むか
1件だけ形が違う場合、全体を止めるのは過剰なことがあります。1万件のうち3件がおかしいだけなら、その3件を記録して残りを処理したい。この判断も事前に決めておく必要があります。
from pydantic import ValidationError
def parse_items(raw_items: list[dict]) -> tuple[list[Item], list[tuple[dict, str]]]: """通ったものと、通らなかったもの(理由つき)を分けて返す。""" ok, rejected = [], [] for raw in raw_items: try: ok.append(Item.model_validate(raw)) except ValidationError as e: rejected.append((raw, str(e))) return ok, rejected
# 使う側では、はじかれた割合を必ず見る# ok, rejected = parse_items(payload["items"])# rate = len(rejected) / max(1, len(payload["items"]))# if rate > 0.01:# raise RuntimeError(f"はじかれた割合が高すぎる: {rate:.1%}")ここで大事なのは、はじいた件数を必ず見ることです。黙って捨てると、ある日から9割がはじかれていても気づきません。割合に上限を設けて、超えたら止めるのが安全です。
はじいたデータそのものも残しておいてください。あとで原因を調べるとき、実際に来た値を見られるかどうかで調査の速さがまったく違います。に応答を残していれば、そちらから辿ることもできます。
型を宣言しておくと、を組み込む場所としても使えます。文字列で来る数値を数値に直す、単位付きの値から単位を外す、といった変換を宣言の一部として書いておけば、取得の入口で形が揃った状態になります。下流のコードから条件分岐が減ります。
この考え方はへの備えそのものです。配信元の変化を、下流ではなく入口で捕まえる。次回は、もっと手前で起きる文字化けの話をします。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。