FastAPIにおけるPydanticを使ったバリデーションのまとめ

# 概要
[FastAPI](https://fastapi.tiangolo.com/)では[Pydantic](https://pydantic-docs.helpmanual.io/)というライブラリを利用してモデルスキーマとバリデーションを宣言的に実装できるようになっている。
ここではその具体的な方法を記述する。

確認したバージョンは以下の通り。
* FastAPI: 0.68.1
* Pydantic: 1.8.2

# 使い方
## モデルの記述と型チェック
### モデルの定義
```python
from pydantic import BaseModel

class Hoge(BaseModel):
    id: int
    name: str
```
のように書けば`Hoge`モデルが作成される。
`Hoge`モデルは整数(`int`)の`id`が必ず存在し、文字列(`str`)の`name`が必ず存在する。この条件を満たさない場合、バリデーションエラーとなる。

### 型
型はPythonの型ヒント(type hints)を使って記述する。

一般的なものを以下に示す。

| 型 | 説明 | JSON schema type |
| -------- | -------- | -------- |
| `None`     | `None`そのもの     | `null`     |
| `bool` | 真偽値 | `boolean` |
| `int` | 整数 | `number` |
| `float` | 浮動小数点数 | `number`|
| `str` | 文字列 | `string` |
| `List` | リスト | `array` |
| `Tuple` | タプル | `array` |
| `Set` | セット | `array`(`uniqueitems=true` となる) |
| `Dict` | 辞書 | `object`|

また、次のものは`datetime`モジュールを`import`して型に使用できる。

| 型 | 説明 | JSONでの対応 |
| -------- | -------- | -------- |
| `datetime.date`     | 日付     |  ISO8601形式の日付を表す文字列、またはUnixタイムスタンプを表す数値 |
| `datetime.time` | 時刻  | ISO8601形式の時刻を表す文字列 |
| `datetime.datetime` | タイムスタンプ（日時） | ISO8601形式の日時を表す文字列、またはUnixタイムスタンプを表す数値 |
| `datetime.timedelta` | 時間間隔 | ISO8601形式の時間間隔を表す文字列、または時間間隔（秒）を表す数値 |


- 型ヒントを使うため、`List[str]`のように要素の型も指定できる
    - JSON schemaでは`items`の`type`の指定になる
- また、`Union`や`Optional`も使用できる
    - `Union`の場合、JSON schemaでは`oneOf`指定になる
    - `Optional`の場合、JSON schemaでは`required`が指定されない


## 必須チェックとデフォルト値
プロパティの必須チェックには次の4パターンの類型がある。

### 厳密な必須チェック（プロパティが存在し値が`None`ではない）
```python
from pydantic import BaseModel

class Hoge(BaseModel):
    hoge: int
```
と書けば次の場合にエラーになる

- `hoge`が存在しない
- `hoge`が存在してもその値が`None`
- `hoge`が整数ではない

### 緩い必須チェック（プロパティが存在すれば値は`None`でもいい）
```python
from pydantic import BaseModel, Field

class Hoge(BaseModel):
    hoge: Optional[int] = Field(...)
```
と書けば次の場合にエラーになる

- `hoge`が存在しない
- `hoge`が整数ではない

### 必須チェックなし（プロパティが存在しなくてもいい）
```python
from pydantic import BaseModel, Field

class Hoge(BaseModel):
    hoge: Optional[int]
```
と書けば次の場合にエラーになる

- `hoge`が整数ではない

### 値が必要なオプション（プロパティは存在しなくてもいいが、存在するなら`null`ではダメなもの）
冗長な書き方が必要となる。

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

class Hoge(BaseModel):
    hoge: Optional[int]
    
    @validator("hoge")                # hogeのバリデーションの登録 
    def validate_hoge(cls, value):    # 関数名はなんでもいい。第1引数はcls固定で使用しない。第2引数はvalueでhogeに設定した値
        if value is None:             # Noneであれば例外を投げる
            raise TypeError("none is not an allowed value") 
        return value                  # Noneでない場合はそのままvalueを返す
```
と書けば次の場合にエラーになる

- `hoge`が`None`
- `hoge`が整数ではない

### デフォルト値
```python
from pydantic import BaseModel, Field

class Hoge(BaseModel):
    hoge: int = 1
    # または
    fuga: int = Field(2)
```
と書けばデフォルト値を設定できる。デフォルト値がある場合は値を明示的に設定しなくてもデフォルト値が設定されていることになるので必須チェックにはかからない。

`Field()` の第1引数にはデフォルト値を設定するが、デフォルト値が不要な場合は、`...`(Ellipsis)を書く。

## 形式チェック
### 文字列の長さ
```python
from pydantic import BaseModel, Field

class Hoge(BaseModel):
    foo: str = Field(..., min_length=2, max_length=10)
```
と書けば、文字数が2文字から10文字以外の場合エラーとなる。
また、デフォルト値の設定が`...`となっているので、値を設定しない場合は必須エラーとなる。

### リストの要素数
```python
from pydantic import BaseModel, Field

class Hoge(BaseModel):
    foo: List[str] = Field(..., min_items=2, max_items=10)
```
と書けば、要素数が2個から10個以外の場合エラーとなる。

### 正規表現
```python
from pydantic import BaseModel, Field

class Hoge(BaseModel):
    foo: str = Field(..., regex=r"[0-9]{2,3}")
```
と書けば、`regex=r"[0-9]{2,3}"`で指定した正規表現にマッチしない場合エラーとなる。

## 値チェック

### 数値の範囲
```python
from pydantic import BaseModel, Field

class Hoge(BaseModel):
    hoge: int = Field(..., ge=2, le=10)
```
と書けば、値が2から10の範囲外の場合エラーとなる。

次の4種類が使用できる

- `ge`: `>=` greater than or equal 以上
- `gt`: `>` greater than 超過
- `le`: `<=`less than or equal 以下
- `lt`: `<` less than 未満

### コード定義
```python
from enum import Enum
from pydantic import BaseModel, Field

class HogeCode(Enum):
    AAA = 1
    BBB = 2
    CCC = 3

class Hoge(BaseModel):
    foo: HogeCode
```
と書けば、値が1, 2, 3以外の場合エラーとなる。


## 複雑なバリデーション
`validator`を実装する。詳しくは[Validators](https://pydantic-docs.helpmanual.io/usage/validators/)を参照。

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

class Hoge(BaseModel):
    hoge: Optional[int]
    
    @validator(プロパティ名) 
    def validate_hoge(cls, value, values):
        if ここでチェック:
            raise ValueError("何かのメッセージ") 
        return value
```

- プロパティ名を@validatorデコレーターに指定するとそのプロパティがチェックされる
- @validatorデコレーターに`each_item=True`を設定するとチェックしたいプロパティが`List`や`Dict`などのコレクションの場合、要素ひとつひとつについてバリデートできる。
- validatorの関数名は任意
- validatorの関数の引数は第1、第2引数は固定、第3引数以降は任意だが`values`という名前の引数を定義すると、そのプロパティより前にバリデートされたプロパティの辞書（プロパティ名がキーで、プロパティの値が値）が入ってくる。
    - これを使って相関チェックが実装できる
- `raise`できる例外クラスは`TypeError` か`ValueError` 自体かサブクラスで詳細は後述する。
    - `raise`せずに`assert`を使って`AssertionError`を投げても問題ない

## Pydanticでは実装できないバリデーション
データベースの内容との相関チェックはPydantic単体ではバリデーションできないため、パスオペレーション関数（`@app.get(...`とかをつけた関数）の先頭でチェックする。

例えば、リクエストボディが次のような形式だとして`qux`にエラーがあった場合は次のように返却する。

```js:request
{
    "foo": {
        "bar": 123,
        "baz": {
            "qux": "errrrrrr"  // これがエラーとする
        }
    }
}
```


```python
from fastapi.exceptions import RequestValidationError
from pydantic.error_wrappers import ErrorWrapper

# ...中略...

if エラー判定:
    raise RequestValidationError(
        [ErrorWrapper(ValueError("エラーメッセージをどうぞ"), ("body", "foo", "baz", "qux"))]
    )
```

`RequestValidationError`を利用することで他のバリデーションエラーと同じ形式で出力される。
エラーレスポンスは次のようになる

**`422 Unprocessable Entity`**

```js:response
{
  "detail": [
    {
      "loc": [
        "body",
        "foo",
        "bar",
        "qux"
      ],
      "msg": "エラーメッセージをどうぞ",
      "type": "value_error"
    }
  ]
}
```

### 例外クラスと`type`についての詳細

- `ErrorWrapper` に指定できる例外クラスは`@validator`で`raise`できるものと同じである
- `ValueError`の場合、レスポンスに出力される`type`は`value_error`となる
- `TypeError`の場合、レスポンスに出力される`type`は`type_error`となる
- `ValueError`のサブクラスを実装しクラス変数として`code="piyopiyo"`を定義すると、レスポンスに出力される`type`は`value_error.piyopiyo`となる。（`TypeError`のサブクラスの場合も同様）