Pydantic入門:  型制約付きフィールドからFastAPI連携まで

## はじめに
前回の記事では、Pydanticの基本的なモデル定義やバリデーションの仕組みを紹介しました。  

https://qiita.com/Tadataka_Takahashi/items/8b28f49d67d7e1d65d11

今回はもう少し応用的な機能として、以下のトピックを取り上げます。

- **1. 型制約付きフィールド**: `constr`, `conint`などを活用した厳格なバリデーション  
- **2. カスタム型とユニオン型**: より複雑なデータを扱うためのヒント  
- **3. field_validatorの使いどころ**: フィールド間の相関チェック  
- **4. BaseSettingsで環境変数管理**: 便利な設定管理機能  
- **5. FastAPIとの簡単な連携例**: API開発との相性抜群のPydanticを体験

それぞれ実装例を交えながら、Google Colabやローカル環境などで試しやすい形で紹介します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/268a7cde-42d9-86fc-8cd2-e566b2a15a0c.png)


---

```python
!pip install pydantic pydantic[email]
```

## 1. 型制約付きフィールド (constr, conintなど)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/045b4334-05fa-8897-ea31-fc0e05516ccc.png)

Pydanticでは標準の型ヒントに加えて、特別な制約を追加できる機能が用意されています。たとえば、文字列の長さや正規表現チェック、数値の最大値・最小値などが簡単に設定可能です。


```python
from pydantic import BaseModel, constr, conint

class Product(BaseModel):
    name: constr(min_length=3, max_length=20)  # 文字列の長さを3〜20文字に制約
    price: conint(ge=0, le=9999)  # 整数に範囲を設定

# 正しいデータ
item = Product(name="Apple", price=120)
print(item)

# 間違ったデータ（価格が範囲外）
try:
    item_ng = Product(name="AB", price=50000)
except ValueError as e:
    print("Validation Error:", e)
```

- **constr** などを使うと、1行で高度な文字列バリデーションが設定できる  
- 価格など数値チェックは **conint** や **confloat** を使うと便利  


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/ebaa6ce5-8882-a3ab-bea2-f42b315f31dd.png)

---

## 2. カスタム型とユニオン型

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/f54e20c7-7178-216e-14e9-e42783b73468.png)

### 2-1. カスタム型 (例: URLやEmailStr など)
Pydanticは標準ライブラリを拡張したカスタム型が用意されています。例えば`EmailStr`や`HttpUrl`などをフィールド型として指定するだけで、形式チェックが自動で行われます。

```python
from pydantic import BaseModel
from pydantic import BaseModel, EmailStr, HttpUrl

class ContactInfo(BaseModel):
    email: EmailStr
    website: HttpUrl

valid_contact = ContactInfo(email="test@example.com", website="https://example.com")
print(valid_contact)

try:
    invalid_contact = ContactInfo(email="invalid-email", website="invalid_url")
except ValueError as e:
    print("Validation Error:", e)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/4419068c-908c-5701-7b8a-aea594fc2da0.png)

### 2-2. ユニオン型 (Union)
複数の型を許容するフィールドには`Union`を使用します。

```python
from typing import Union
from pydantic import BaseModel

class Payment(BaseModel):
    amount: Union[int, str]  # 数値または文字列を許可

pay_data1 = Payment(amount=1000)
pay_data2 = Payment(amount="FREE")
print(pay_data1, pay_data2)

try:
    pay_data3 = Payment(amount=["NG"])
except ValueError as e:
    print("Validation Error:", e)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/c1e4d5c8-0022-20d6-69c8-6c30c94f1a69.png)

---

## 3. field_validatorの使いどころ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/f16579aa-6af0-3643-16eb-cce6d630d32a.png)

単一のフィールドごとの検証： @field_validator
モデル全体（複数フィールドの相関チェックなど）： @model_validator

```python
from pydantic import BaseModel, model_validator

class Order(BaseModel):
    product_name: str
    quantity: int
    price_per_item: float

    @model_validator(mode="after")
    def total_price_must_not_exceed_limit(cls, model):
        total_price = model.quantity * model.price_per_item
        if total_price > 100000:
            raise ValueError(f"Total price ({total_price}) exceeds the allowed limit.")
        return model

# 正しいデータ
order_ok = Order(product_name="Computer", quantity=5, price_per_item=10000)
print(order_ok)

# エラーとなるデータ
try:
    order_ng = Order(product_name="Computer", quantity=50, price_per_item=3000)
except ValueError as e:
    print("Validation Error:", e)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/4a8eb1a1-41ce-e2c9-ffa1-8d4ee17d9c35.png)

---

## 4. BaseSettingsで環境変数管理

Pydanticには、設定ファイルや環境変数から値を読み込むための`BaseSettings`クラスがあります。

```python
!pip install pydantic-settings
```

```python
import os
from pydantic_settings import BaseSettings, SettingsConfigDict

# 環境変数の例
os.environ["API_KEY"] = "your_secret_key_123"


class AppConfig(BaseSettings):
    api_key: str
    debug_mode: bool = False

    model_config = SettingsConfigDict(env_file=".env")

# 設定の取得と表示
config = AppConfig()
print("API Key:", config.api_key)
print("Debug Mode:", config.debug_mode)
```
---

## 5. FastAPIとの簡単な連携例

```python
!pip install fastapi uvicorn
```

```python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    name: str
    age: int

@app.post("/users/")
def create_user(user: User):
    return {"message": f"User {user.name} with age {user.age} created!"}
```

- `uvicorn main:app --reload` でサーバーを起動し、APIを確認できます。

---

## まとめ
- Pydantic v2では、`@field_validator`を活用して柔軟なバリデーションが可能
- 型制約付きフィールドやカスタム型を使用することで、データ整合性を強化
- `BaseSettings`を使用して環境変数や設定ファイルを管理
- FastAPIとの連携でバリデーションをAPIに統合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/3deb1862-7689-86c6-7d36-9f417105717d.png)

---

## 参考リンク
- [Pydantic公式ドキュメント](https://docs.pydantic.dev/)
- [FastAPI公式ドキュメント](https://fastapi.tiangolo.com/)