Pydantic 忘備録

# Pydanticの基本

以下をnotebookのcellに入れて実験しましょう。

## 基本編

### 準備

- `BaseModel`: 
    - Pydantic の核心となるクラス
    - これを継承してモデルクラスを作ることで、各種バリデーション機能が利用できるようになります
- `ValidationError`: 
    - Pydantic が検証時に投げる例外クラス
    - 不正なデータを与えるとこのエラーが発生します
- `User`:
    - `BaseModel` を継承したクラス
    - アノテーション (id: int, name: str など) によって、各フィールドの型を指定します。
    - 必須フィールド（... などを指定していない場合）はアノテーションだけで十分指定可能です。


```python:cell1
from pydantic import BaseModel, ValidationError

class User(BaseModel):
    """
    ユーザー情報を保持するシンプルな Pydantic モデルクラス
    """
    id: int
    name: str
    age: int
```

### 正常系のテスト

以下を実行すると

```python:cell2
# 1) 正しいデータを与えてインスタンス化する例
valid_data = {
	"id": 1,
	"name": "Alice",
	"age": 30,
}
try:
	user = User(**valid_data)  # 通常のコンストラクタ
	print("[OK] 正常にインスタンス化:", user)
except ValidationError as e:
	print("[ERROR] ValidationError:", e)
```

```terminal
[OK] 正常にインスタンス化: id=1 name='Alice' age=30
```

### 異常系のテスト

以下を実行すると

```python:cell2
# 2) 不正なデータ(型不一致や欠落)を与えてインスタンス化する例
invalid_data = {
	"id": "文字列になっている",		# 本来 int が必要
	# "name": "Bob",			# name を敢えて入れない例なども可
	"age": 25,
}
try:
	user = User(**invalid_data)
	print("[OK] 正常にインスタンス化:", user)
except ValidationError as e:
	print("[ERROR] ValidationError:", e)
```

```terminal
[ERROR] ValidationError: 2 validation errors for User
id
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='文字列になっている', input_type=str]
    For further information visit https://errors.pydantic.dev/2.10/v/int_parsing
name
  Field required [type=missing, input_value={'id': '文字列になっている', 'age': 25}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.10/v/missing
```

## `model_construct`メソッドによるバリデーション無しモデル生成

### 準備

```python
from pydantic import BaseModel, ValidationError, Field

class User(BaseModel):
    """
    Pydantic の基本クラス BaseModel を継承して、
    ユーザー情報を表すためのモデルクラスを定義します。
    """
    
    id: int = Field(..., description="ユーザーID（必須）")
    name: str = Field(..., description="ユーザー名（必須）")
    age: int = Field(0, description="ユーザーの年齢（デフォルトは 0）")

    class Config:
        """
        Config（model_config）クラスでは、
        extra フィールドの扱い方や型チェックの動作などを設定できます。
        """
        extra = "ignore"  # ここでは未知のフィールドが来ても無視します。
```

### 通常のインスタンス生成

通常のコンストラクタ（`User(**values)`）によるモデルインスタンス生成を示す関数

- Pydantic が自動で型・必須フィールドのチェックを行うので安全
- 不正なデータがあれば ValidationError を投げる

```python
valid_data = {
	"id": 1,
	"name": "Alice",
	"age": 25,
	"extra_field": "このフィールドは ignore 設定により無視される",
}

try:
	# バリデーションを通してインスタンス化
	user = User(**valid_data)
	print(f"[OK] 正常にインスタンス化: {user}")
except ValidationError as e:
	# 型が合わない、必須フィールドがないなど不備があればこのエラーが発生
	print("[ERROR] ValidationError:", e)
```

```terminal
[OK] 正常にインスタンス化: id=1 name='Alice' age=25
```

### 不正なデータを使ったインスタンス生成

無効なデータ（型が合わない、必須フィールドがないなど）を渡した場合の例。ValidationError が発生する様子を確認します。

```python
invalid_data = {
	"id": "文字列は本来 int ではない",  # 型が合わない
	"name": "Bob",
	# "age" フィールドが欠落している（任意だが、型違いでテストを兼ねている）
}

try:
	user = User(**invalid_data)
	print(f"[OK] 正常にインスタンス化されてしまいました: {user}")
except ValidationError as e:
	# ここでは必ず ValidationError が起こるはず
	print("[ERROR] ValidationError:", e)
```

```terminal
[ERROR] ValidationError: 2 validation errors for User
id
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='文字列は本来 int ではない', input_type=str]
    For further information visit https://errors.pydantic.dev/2.10/v/int_parsing
age
  Field required [type=missing, input_value={'id': '文字列は本...ない', 'name': 'Bob'}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.10/v/missing
```


### バリデーション無しでインスタンス生成

Pydantic の `model_construct` メソッドを利用して、バリデーションを行わずにモデルを生成する方法を示す関数。

- このメソッドは「信頼できるデータ」あるいは「すでにバリデーション済みのデータ」を再度 Pydantic のモデルにしたい場合など、性能を重視する用途で用いられます
- バリデーションを一切行わないため、誤ったデータが混在していてもエラーは出ません

```python
trusted_data = {
	"id": 999,        # ここでは int 型で正しい値を入れる
	"name": "Charlie", 
	"age": "30",      # 文字列だが、バリデーションがないためこのまま通ってしまう
	"extra_field": "このフィールドも ignore 設定により無視される",
}

# ↓ バリデーションなしで直接モデルを生成
user = User.model_construct(**trusted_data)
print("[INFO] バリデーションなしで生成した User: ", user)
print("[INFO] 内部の __dict__ を確認:", user.__dict__)  # age が str のまま入っている！
```

```terminal
[INFO] バリデーションなしで生成した User:  id=999 name='Charlie' age='30'
[INFO] 内部の __dict__ を確認: {'id': 999, 'name': 'Charlie', 'age': '30'}
```

## ConfigDictを使った例

Pydantic v2 から登場した ConfigDict は、Pydantic v1 系で用いられていた class Config: とほぼ同等の機能を、辞書形式で指定できるようにしたものです。これを `model_config` というクラス属性に割り当てると、**Pydantic モデルの挙動を細かく設定**できます。

### 準備

- `from pydantic import BaseModel, ConfigDict` をインポート
- `BaseModel`: Pydantic のモデルを作成するときに継承するクラス
- `ConfigDict`: モデルの設定（コンフィグ）を辞書形式でまとめるためのヘルパーモデルクラス内で `model_config` というクラス変数を定義し、`ConfigDict` を代入

```python
from pydantic import BaseModel, ConfigDict

class User(BaseModel):
    # ここで設定を辞書的にまとめる
    model_config = ConfigDict(
        extra='ignore',           	# 未定義フィールドを無視する
        validate_assignment=True,  	# 既存インスタンスに新たな値を設定した際もバリデーションする
        frozen=True,             	# True にすると、インスタンスのフィールドを変更不可にできる
        title='User Model',       	# スキーマなどで表示されるタイトル
    )

    id: int
    name: str
    age: int = 0
```

### test1: 通常どおりインスタンス化

```python
user_data = {'id': 123, 'name': 'Alice', 'age': 25, 'extra_field': '無視される'}
user = User(**user_data)
print(user)
```

```terminal
id=123 name='Alice' age=25
```

### test2: `validate_assignment=True`による代入時のエラーチェック

validate_assignment=True により、代入時にもチェックが行われることを確認する

```python
user.age = '30'  # int 型以外を入れようとすると ValidationError が起きる
```

```terminal
---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
Cell In[25], line 2
      1 # validate_assignment=True により、代入時にもチェックが行われる
----> 2 user.age = '30'  # int 型以外を入れようとすると ValidationError が起きる

File ~/himawari/latest/venv/lib/python3.10/site-packages/pydantic/main.py:911, in BaseModel.__setattr__(self, name, value)
    908             self.__pydantic_private__[name] = value
    909     return
--> 911 self._check_frozen(name, value)
    913 attr = getattr(self.__class__, name, None)
    914 # NOTE: We currently special case properties and `cached_property`, but we might need
    915 # to generalize this to all data/non-data descriptors at some point. For non-data descriptors
    916 # (such as `cached_property`), it isn't obvious though. `cached_property` caches the value
    917 # to the instance's `__dict__`, but other non-data descriptors might do things differently.

File ~/himawari/latest/venv/lib/python3.10/site-packages/pydantic/main.py:986, in BaseModel._check_frozen(self, name, value)
    980     return
    981 error: pydantic_core.InitErrorDetails = {
    982     'type': typ,
    983     'loc': (name,),
    984     'input': value,
    985 }
--> 986 raise pydantic_core.ValidationError.from_exception_data(self.__class__.__name__, [error])

ValidationError: 1 validation error for User
age
  Instance is frozen [type=frozen_instance, input_value='30', input_type=str]
    For further information visit https://errors.pydantic.dev/2.10/v/frozen_instance
```