pydanticによる型検証 [BaseModel]

## はじめに

Pythonは動的な型付けであるため，変数の型が保証されていない。型ヒントによる宣言や`mypy`によるチェックは可能だが実行時の制限はない。
そこで，`pydantic`による型に堅牢な書き方を記録する。

## TL;DR

- `pydantic`を使用すると簡単に型検証が行える
  - `pydantic.BaseModel`の継承と型ヒント
- 型の宣言は`typing`などによる型ヒントで行う

## pydanticとは

下記は[公式ドキュメント](https://pydantic-docs.helpmanual.io/)による説明です。

  > - Data validation and settings management using Python type annotations.
  > - pydantic enforces type hints at runtime, and provides user friendly errors when data is invalid.
  > - Define how data should be in pure, canonical Python; validate it with pydantic.

- google 翻訳

  > - Pythonの型アノテーションを使ったデータ検証・設定管理
  > - pydanticは実行時に型ヒントを強制し，データが無効な場合はユーザーフレンドリーなエラーを提供
  > - データがどのようにあるべきかを純粋で標準的なPythonで定義，検証

### インストール

`pydantic`はpythonの標準モジュールではないので下記コマンドによるインストールが必要
`pip install pydantic`

## 使用方法

バージョンは以下のとおり

- Python: 3.10.5
- Pydantic: 1.10.2

以下サンプルコードは`import pydantic`されている前提

### 基本 `pydantic.BaseModel`

pydanticのBaseModelを継承して使用する。また，型宣言は型ヒントにより記述する。
型宣言に対して不整合な値が入力された場合はエラーが発生する。しかし，宣言と異なる型の値でもキャストが可能な場合はキャストして代入される。厳密な型検証を行いたい場合は次項を参照  

```python:model.py
class User(pydantic.BaseModel):
    id_: int
    name: str


print(f'{User(id_=100, name="abc")   = }')
print(f'{User(id_=100.0, name="abc") = }')
print(f'{User(id_="100", name="abc") = }')
try:
    User(id_="abc", name="abc")
except pydantic.ValidationError as e:
    print("User(id_='abc', name='abc') = ")
    print(f"{type(e).__name__}: {e}")

# User(id_=100, name="abc")   = User(id_=100, name='abc')
# User(id_=100.0, name="abc") = User(id_=100, name='abc')
# User(id_="100", name="abc") = User(id_=100, name='abc')
# User(id_='abc', name='abc') =
# ValidationError: 1 validation error for User
# id_
#   value is not a valid integer (type=type_error.integer)
```

`bool`を宣言した場合は以下のように[キャスト](https://pydantic-docs.helpmanual.io/usage/types/#booleans)される。  

- False: `False, 0, '0', 'off', 'f', 'false', 'n', 'no'`
- True: `True, 1, '1', 'on', 't', 'true', 'y', 'yes'`

```python:model.py
class BooleanModel(pydantic.BaseModel):
    is_enabled: bool


print(f'{BooleanModel(is_enabled="on")  = }')
print(f'{BooleanModel(is_enabled="off") = }')
try:
    BooleanModel(is_enabled=-1)
except pydantic.ValidationError as e:
    print("BooleanModel(is_enabled=-1) = ")
    print(f"{type(e).__name__}: {e}")

# BooleanModel(is_enabled="on")  = BooleanModel(is_enabled=True)
# BooleanModel(is_enabled="off") = BooleanModel(is_enabled=False)
# BooleanModel(is_enabled=-1) =
# ValidationError: 1 validation error for BooleanModel
# is_enabled
#   value could not be parsed to a boolean (type=type_error.bool)
```

### 初期値と必須項目

初期値を設定することが可能であり，その場合は必須項目ではなくなる。初期値を設定する場合は型宣言を行わなくとも初期値から型が推定される。  

```python:model.py
class UserWithNumber(pydantic.BaseModel):
    id_: int
    name: str = "abc"
    number = 0


print(f"{UserWithNumber(id_=100) = }")
try:
    UserWithNumber()
except pydantic.ValidationError as e:
    print("UserWithNumber() = ")
    print(f"{type(e).__name__}: {e}")

try:
    UserWithNumber(id_=100, number="abc")
except pydantic.ValidationError as e:
    print("UserWithNumber(id_=100, number='abc') = ")
    print(f"{type(e).__name__}: {e}")


# UserWithNumber(id_=100) = UserWithNumber(id_=100, name='abc', number=0)
# UserWithNumber() =
# ValidationError: 1 validation error for UserWithNumber
# id_
#   field required (type=value_error.missing)
# UserWithNumber(id_=100, number='abc') =
# ValidationError: 1 validation error for UserWithNumber
# number
#   value is not a valid integer (type=type_error.integer)
```

### 複数の型宣言とNull許容

型宣言には複数の型を宣言することが可能。その場合は左の型から優先される。  
`None`を合わせて宣言することでNullを許容することができる。(新しいバージョンでは[**初期値とNull許容は別**になる予定](https://pydantic-docs.helpmanual.io/blog/pydantic-v2/#required-vs-nullable-cleanup))

```python:model.py
class NullableModel(pydantic.BaseModel):
    id_: int | None
    name: str


print(f'{NullableModel(id_=100, name="abc")  = }')
print(f'{NullableModel(id_=None, name="abc") = }')
print(f'{NullableModel(name="abc")           = }')
try:
    NullableModel(name=None)
except pydantic.ValidationError as e:
    print("NullableModel(name=None) = ")
    print(f"{type(e).__name__}: {e}")

# NullableModel(id_=100, name="abc")  = NullableModel(id_=100, name='abc')
# NullableModel(id_=None, name="abc") = NullableModel(id_=None, name='abc')
# NullableModel(name="abc")           = NullableModel(id_=None, name='abc')
# NullableModel(name=None) =
# ValidationError: 1 validation error for NullableModel
# name
#   none is not an allowed value (type=type_error.none.not_allowed)

```

### 厳密な型検証

型検証では宣言された型にキャストされる。そのため，int型にfloat値を入力すると小数点以下は切り捨てられる。  
厳密に型を検証したい場合は`pydantic.StrictInt`や`pydantic.StrictBool`などの[厳密な型](https://pydantic-docs.helpmanual.io/usage/types/#strict-types)を指定する。(新しいバージョンでは [**Strict Mode** が導入される予定](https://pydantic-docs.helpmanual.io/blog/pydantic-v2/#strict-mode))  

```python:model.py
class StrictIntModel(pydantic.BaseModel):
    strict_int: pydantic.StrictInt


try:
    StrictIntModel(strict_int=3.14159)
except pydantic.ValidationError as e:
    print("StrictIntModel(strict_int=3.14159) = ")
    print(f"{type(e).__name__}: {e}")

# StrictIntModel(strict_int=3.14159) =
# ValidationError: 1 validation error for StrictIntModel
# strict_int
#   value is not a valid integer (type=type_error.integer)


class StrictBoolModel(pydantic.BaseModel):
    strict_bool: pydantic.StrictBool


try:
    StrictBoolModel(strict_bool="False")
except pydantic.ValidationError as e:
    print("StrictBoolModel(strict_bool='False') = ")
    print(f"{type(e).__name__}: {e}")

# StrictBoolModel(strict_bool='False') =
# ValidationError: 1 validation error for StrictBoolModel
# strict_bool
#   value is not a valid boolean (type=value_error.strictbool)
```

## 参考文献

- [Pydantic 公式ドキュメント](https://pydantic-docs.helpmanual.io/)
- [PydanticがRustで爆速になるという話]([URL](https://qiita.com/ksato9700/items/053e06f795d8a9b5d706))
- [FastAPIを使っていなくても、Pydanticは便利です](https://attsun1031.github.io/blog/pydantic-intro)