pydanticレビュー:構造定義とバリデーションの責務分離を確認する
はじめに
pydanticは、Pythonでのデータ構造定義とバリデーションを統一的に扱える強力なライブラリとして広く使われています。
しかしその一方で、「データ構造の定義」と「バリデーション・変換ロジック」が1つのクラスに混在しやすく、
レビュー時に責務の不明瞭化、ロジックの重複、意図の不透明さといった問題に直面することがあります。
本稿では、レビューアーがpydanticモデルを評価する際の観点を「責務の分離」に着目して整理します。
基本構造:最小のpydanticモデル
基本モデル
from pydantic import BaseModel
class User(BaseModel):
id: int
name: strこのようなモデルは、構造とバリデーションが一致しており、レビュー観点では特に問題ありません。
しかし、以下のようにバリデーション・変換ロジックが追加されていく過程で、設計の歪みが現れ始めます。
問題例:ロジックが混在するpydanticモデル
構造とバリデーションの混在
from pydantic import BaseModel, validator
class Product(BaseModel):
name: str
price: float
currency: str
@validator('currency')
def normalize_currency(cls, v):
return v.upper()
@validator('price')
def check_positive(cls, v):
if v <= 0:
raise ValueError('Price must be positive')
return vレビュー指摘
@Reviewer: このモデルは構造定義・正規化・値検証が一体化しており、可読性とテスト性に課題が生じます。
変換処理(normalize)と制約チェック(validation)は分離して記述できるよう構造を整理すべきです。責務分離の考え方
pydanticモデルには、次のような異なる責務が自然と入り込みます。
| 責務 | 具体例 | レビュー観点 |
|---|---|---|
| 構造定義 | 型ヒント、フィールド名 | 過不足ない構造になっているか |
| 入力正規化 | 大文字化、フォーマット変換 | UIや外部API依存の処理が混入していないか |
| 値検証 | 範囲チェック、null制限 | ビジネスルールに依存していないか |
| 表現変換 | @propertyによる派生値計算 |
モデルから分離した構造でも実現可能か |
混在モデルと分離モデルの対比
この図では、ProductModel にバリデーションロジックが集中している構造が確認できます。
責務ごとにロジックを分離すれば、CleanProductのような純粋なデータ構造に近づけることが可能です。
レビューで確認すべきpydanticモデルの観点
validatorに状態変換ロジック(正規化・加工)が含まれていないか?- モデルにビジネスロジックが入り込んでいないか?
- フィールド定義とロジックが過度に混在していないか?
- 出力形式や内部状態保持に
@propertyや@root_validatorが乱用されていないか?
リファクタリング例:責務を分けた構造
正規化処理の切り出し
def normalize_currency(value: str) -> str:
return value.upper()
def validate_price(value: float) -> float:
if value <= 0:
raise ValueError("Price must be positive")
return value
class Product(BaseModel):
name: str
price: float
currency: str
@classmethod
def from_input(cls, data: dict) -> "Product":
data['currency'] = normalize_currency(data['currency'])
data['price'] = validate_price(data['price'])
return cls(**data)分離構造の評価
@Reviewer: 検証・正規化が明示的に関数分離され、データ定義と処理責務が分かれています。
テスト可能性や拡張性に優れた構造となっています。root_validatorの乱用に注意
root_validatorのリスク
from pydantic import BaseModel, root_validator
class Settings(BaseModel):
host: str
port: int
@root_validator
def validate_config(cls, values):
if values['host'] == 'localhost' and values['port'] == 80:
raise ValueError("Port 80 not allowed on localhost")
return valuesレビューコメント
@Reviewer: `@root_validator`は複数フィールドの相関検証に適していますが、複雑な条件が入り込むとデバッグ・再利用が困難になります。
外部関数化や別責務クラスへの委譲も検討すべきです。モデルとロジックを分離する構造的指針
BaseModelには構造と基本的な型制約のみに限定する- 入力正規化・変換は専用の関数やファクトリクラスへ移譲
- ビジネスロジックや制約条件はドメインロジック層に引き上げ
- バリデーションと変換の混在はテスト困難・再利用困難につながる
結論:pydanticモデルは「構造定義のための道具」であることを忘れない
レビューアーとして意識すべきは、**pydanticを便利なライブラリとして使うか、構造的設計の一部として :::
