0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【FastAPI入門 #2】Pydanticで学ぶ型バリデーション ── 「型を書くだけ」の裏側を理解する

0
Posted at

はじめに

前回の記事では、FastAPIを使うと型定義だけでバリデーションが自動になると紹介しました。

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

でも、「なぜこれだけでバリデーションが動くの?」という疑問は残りますよね。

今回はその裏側を担っている Pydantic に焦点を当てて、実務でよく使うパターンを一通り解説します。


1. Pydanticとは?

Pydantic は、Pythonの型ヒントを使ってデータのバリデーションと変換を行うライブラリです。FastAPIはPydanticを内部で使っており、FastAPIを入れると自動でインストールされます。

pip install pydantic  # FastAPIと一緒に入るので通常は不要

一番シンプルな使い方はこうです:

from pydantic import BaseModel

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

# 正しいデータ → そのまま使える
user = User(name="Alice", age=30)
print(user.name)  # Alice

# 間違ったデータ → ValidationError が発生
user = User(name="Alice", age="三十")  # ageに文字列を渡した
# → pydantic_core._pydantic_core.ValidationError: ...

FastAPIでは、このバリデーションがリクエストを受け取った瞬間に自動で走ります。エラーがあれば 422 Unprocessable Entity を返してくれます。


2. 基本の型ヒント

Pydanticで使える主な型を整理します。

from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime

class Item(BaseModel):
    name: str               # 文字列(必須)
    price: float            # 浮動小数点(必須)
    stock: int              # 整数(必須)
    is_available: bool      # 真偽値(必須)
    tags: List[str]         # 文字列のリスト(必須)
    description: Optional[str] = None  # 文字列 or None(任意、デフォルトNone)
    created_at: datetime    # 日時(必須)

💡 Optional[str] とは?
str または None のどちらでも受け付ける、という意味です。
Optional[str] = None と書くと「省略可能なフィールド」になります。


3. Fieldでバリデーションを細かく制御する

型だけでは「文字列であること」しか保証できません。
「文字列で、かつ1〜50文字以内」のような詳細な制約には Field を使います。

from pydantic import BaseModel, Field

class UserCreate(BaseModel):
    name: str = Field(
        min_length=1,       # 最小文字数
        max_length=50,      # 最大文字数
        description="ユーザー名"  # ドキュメント用の説明
    )
    age: int = Field(
        ge=0,   # greater than or equal(0以上)
        le=120, # less than or equal(120以下)
        description="年齢"
    )
    email: str = Field(
        pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$",  # 正規表現でメール形式を確認
        description="メールアドレス"
    )
    score: float = Field(
        default=0.0,  # デフォルト値
        ge=0.0,
        le=100.0,
        description="スコア(0.0〜100.0)"
    )

Fieldの主要なオプション一覧

オプション 意味 対象
default デフォルト値 全型
min_length 最小文字数 str
max_length 最大文字数 str
pattern 正規表現パターン str
ge 以上(≥) int, float
le 以下(≤) int, float
gt より大きい(>) int, float
lt より小さい(<) int, float
description Swagger UIに表示される説明 全型
examples Swagger UIに表示されるサンプル値 全型

4. ネストしたモデル(モデルの中にモデル)

実務では、データが入れ子構造になることがよくあります。

from pydantic import BaseModel, Field
from typing import List

class Address(BaseModel):
    prefecture: str = Field(description="都道府県")
    city: str       = Field(description="市区町村")
    zipcode: str    = Field(pattern=r"^\d{3}-\d{4}$", description="郵便番号(例: 100-0001)")

class UserCreate(BaseModel):
    name: str
    age: int
    address: Address        # モデルをそのまま型として使える
    tags: List[str] = []    # リストのデフォルトは []

リクエストのJSONはこうなります:

{
  "name": "Alice",
  "age": 30,
  "address": {
    "prefecture": "東京都",
    "city": "千代田区",
    "zipcode": "100-0001"
  },
  "tags": ["admin", "user"]
}

Pydanticはネストした Address の中のバリデーションもすべて自動で走らせます
zipcode"100-001" のようなフォーマット違いでもきちんとエラーになります。


5. よくあるエラーと原因

エラー例1:型の不一致

class Item(BaseModel):
    price: float

Item(price="高い")  # 文字列を渡した
ValidationError: 1 validation error for Item
price
  Input should be a valid number [type=float_parsing, ...]

エラー例2:必須フィールドの欠落

class Item(BaseModel):
    name: str
    price: float

Item(name="りんご")  # price を渡し忘れた
ValidationError: 1 validation error for Item
price
  Field required [type=missing, ...]

エラー例3:Fieldの制約違反

class UserCreate(BaseModel):
    age: int = Field(ge=0, le=120)

UserCreate(age=200)  # 120を超えた
ValidationError: 1 validation error for UserCreate
age
  Input should be less than or equal to 120 [type=less_than_equal, ...]

FastAPIではこれらのエラーが自動で整形されて、クライアントに 422 レスポンスとして返ります。自分でtry/exceptを書く必要はありません。


6. FastAPIでの実践例

ここまでの内容をFastAPIに組み込んでみます。

from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Optional, List

app = FastAPI()

class Address(BaseModel):
    prefecture: str
    city: str
    zipcode: str = Field(pattern=r"^\d{3}-\d{4}$")

class UserCreate(BaseModel):
    name: str    = Field(min_length=1, max_length=50, description="ユーザー名")
    age: int     = Field(ge=0, le=120, description="年齢")
    email: str   = Field(pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$", description="メール")
    address: Address
    tags: List[str] = []
    bio: Optional[str] = None

class UserResponse(BaseModel):
    id: int
    name: str
    age: int
    email: str

@app.post("/users", response_model=UserResponse, status_code=201)
def create_user(user: UserCreate):
    # 実際はDBに保存する処理が入る
    # ここではダミーのIDを返す
    return UserResponse(id=1, name=user.name, age=user.age, email=user.email)

💡 response_model とは?
レスポンスの形を定義するモデルです。
UserCreate に含まれる addresstags を返したくないとき、
UserResponse で返すフィールドだけを定義すれば自動でフィルタリングされます。
パスワードなど、入力は受けるが返してはいけないフィールドの隠蔽に便利です。


7. まとめ

Pydanticを使うと:

  • strintfloat などの基本型で型チェックが自動になる
  • Field を使えば文字数制限・数値範囲・正規表現などの詳細な制約を追加できる
  • モデルを入れ子にすることで複雑なデータ構造にも対応できる
  • バリデーションエラーは FastAPI が自動で 422 レスポンスにしてくれる
  • response_model でレスポンスのフィールドを制御できる

次回は パスパラメータ・クエリパラメータ・リクエストボディの使い分け を解説します。「URLの {id}?page=2 は何が違うの?」という疑問をコードと図で整理します。


参考


この記事は学習日記として書いています。間違いや補足があればコメントいただけると嬉しいです!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?