型を活用した Pydanitc の共有バリデーションの設計
Pydantic は Python のデータクラスを定義するためのライブラリ。Pydantic を使用するとデータクラスのフィールドに対して型アノテーションを追加し、データのバリデーションを実行できる。Pydantic のバリデーションを共通化するための設計について考えてみた。
バリデーションを共有するモチベーション
システムの設計上、同じ条件でバリデーションを実行したい場合がある。例えば、クライアントサイドからデータ送信する前のバリデーションとサーバーでデータを受け取る前のバリデーションの共通化や、データ処理の整合性を保つために同じバリデーションを実行したい。
バリデーションとフィールドの共有
Pydantic のデータクラスを定義する際にフィールドに対してバリデーションを追加できる。例えば、次のようにField
クラスを使用してバリデーションを追加できる。
| from pydantic import BaseModel, Field
class User(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
age: int = Field(..., ge=0, le=150)
|
バリデーションやフィールドの設定を共通化するために、次のようにUser
を継承したクラスを作成できる。
| from pydantic import BaseModel, Field
class AdminUser(User):
role: str = Field(..., min_length=1, max_length=100)
class NormalUser(User):
pass
|
上記のようにクラス継承をすることで、フィールドやバリデーションを共通化できる。しかし、このようなクラス継承の問題点として、次のようなものがある。
- 継承元のクラスを小分けにすることで、継承先のフィールドを細かく設定できるが、最終的に 1 行のフィールドが記述されたクラスが乱立する
- 深い継承は可読性を損なう
- mixin で複数のクラスを継承する際に、同じフィールドに対してどのクラスのバリデーションを優先するかが不明瞭
バリデーションの共通化
クラス継承の問題点を解決しつつ、バリデーションを共通化するための設計を考えてみる。次のように型アノテーションを使用して、バリデーションを共通化できる。
| from typing import Annotated
from pydantic import (
AfterValidator,
PlainSerializer,
TypeAdapter,
WithJsonSchema,
)
Name = Annotated[
str,
AfterValidator(lambda x: x.strip()),
PlainSerializer(lambda x: x.upper(), return_type=str),
WithJsonSchema({'type': 'string'}, mode='serialization'),
]
Age = Annotated[
int,
AfterValidator(lambda x: max(0, min(x, 150))),
PlainSerializer(lambda x: f'{x:03}', return_type=str),
WithJsonSchema({'type': 'number'}, mode='serialization'),
]
class User(BaseModel):
name: Name
age: Age
class Person(BaseModel):
age: Age
address: str
class Game(BaseModel):
host_name: Name
guest_name: Name
team: str
|
上記のように、型アノテーションを使用してバリデーションを共通化できる。型を活用したバリデーションの共通化は、クラス継承の課題を解決しているのに加え、バリデーションの再活用がしやすい。個別のフィールドに対するバリデーションを共通化することで、データの整合性を保つことができる。
参考リンク