コンテンツにスキップ

型を活用した Pydanitc の共有バリデーションの設計

Pydantic は Python のデータクラスを定義するためのライブラリ。Pydantic を使用するとデータクラスのフィールドに対して型アノテーションを追加し、データのバリデーションを実行できる。Pydantic のバリデーションを共通化するための設計について考えてみた。

バリデーションを共有するモチベーション

システムの設計上、同じ条件でバリデーションを実行したい場合がある。例えば、クライアントサイドからデータ送信する前のバリデーションとサーバーでデータを受け取る前のバリデーションの共通化や、データ処理の整合性を保つために同じバリデーションを実行したい。

バリデーションとフィールドの共有

Pydantic のデータクラスを定義する際にフィールドに対してバリデーションを追加できる。例えば、次のようにFieldクラスを使用してバリデーションを追加できる。

1
2
3
4
5
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を継承したクラスを作成できる。

1
2
3
4
5
6
7
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

上記のように、型アノテーションを使用してバリデーションを共通化できる。型を活用したバリデーションの共通化は、クラス継承の課題を解決しているのに加え、バリデーションの再活用がしやすい。個別のフィールドに対するバリデーションを共通化することで、データの整合性を保つことができる。

参考リンク