コンテンツにスキップ

追加モデル

前の例に引き続き、複数の関連モデルを持つことは一般的です。

これは特にユーザーモデルの場合に当てはまります。なぜなら

  • **入力モデル**はパスワードを持つことができる必要があるためです。
  • **出力モデル**はパスワードを持つべきではないためです。
  • **データベース モデル**はおそらくハッシュ化されたパスワードを持つ必要があるためです。

危険

ユーザーの平文パスワードを保存しないでください。常に検証できる「セキュアハッシュ」を保存してください。

知らない場合は、セキュリティの章で「パスワードハッシュ」とは何かを学ぶことができます。

複数のモデル

パスワードフィールドとその使用場所を持つモデルの一般的な例を次に示します。

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserInDB(BaseModel):
    username: str
    hashed_password: str
    email: EmailStr
    full_name: str | None = None


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: Union[str, None] = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: Union[str, None] = None


class UserInDB(BaseModel):
    username: str
    hashed_password: str
    email: EmailStr
    full_name: Union[str, None] = None


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved

情報

Pydantic v1では、メソッドは.dict()と呼ばれていましたが、Pydantic v2では非推奨(ただし、まだサポートされています)となり、.model_dump()に名前が変更されました。

ここでの例では、Pydantic v1との互換性のために.dict()を使用していますが、Pydantic v2を使用できる場合は、代わりに.model_dump()を使用する必要があります。

**user_in.dict() について

Pydanticの.dict()

user_in は、クラスUserIn のPydanticモデルです。

Pydanticモデルには、モデルのデータを含むdictを返す.dict()メソッドがあります。

そのため、次のようにPydanticオブジェクトuser_inを作成した場合

user_in = UserIn(username="john", password="secret", email="john.doe@example.com")

そして、次のように呼び出した場合

user_dict = user_in.dict()

これで、変数user_dictにデータを含むdictができました(Pydanticモデルオブジェクトではなく、dictです)。

そして、次のように呼び出した場合

print(user_dict)

次のようなPython dictが取得されます。

{
    'username': 'john',
    'password': 'secret',
    'email': 'john.doe@example.com',
    'full_name': None,
}

dict のアンラップ

user_dict のような dict**user_dict として関数(またはクラス)に渡すと、Python はそれを「展開」します。 user_dict のキーと値が、そのままキーと値の引数として渡されます。

つまり、上記の user_dict を使って続けると、

UserInDB(**user_dict)

と書くことは、

UserInDB(
    username="john",
    password="secret",
    email="john.doe@example.com",
    full_name=None,
)

と書くことと同じになります。あるいは、より正確には、将来どのような内容になるかわからない user_dict を直接使用すると、

UserInDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
)

他の Pydantic モデルの内容から Pydantic モデルを作成する

上記の例では、user_dictuser_in.dict() から取得したので、このコードは

user_dict = user_in.dict()
UserInDB(**user_dict)

は、

UserInDB(**user_in.dict())

と同じになります。なぜなら、user_in.dict()dict であり、それを ** を前に付けて UserInDB に渡すことで Python に「展開」させるからです。

つまり、別の Pydantic モデルのデータから Pydantic モデルを取得します。

dict の展開と追加のキーワード引数

そして、

UserInDB(**user_in.dict(), hashed_password=hashed_password)

のように、追加のキーワード引数 hashed_password=hashed_password を追加すると、最終的には

UserInDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
    hashed_password = hashed_password,
)

警告

補助的な関数 fake_password_hasherfake_save_user は、データの考えられる流れを示すためのものであり、実際にはセキュリティを提供するものではありません。

重複の削減

コードの重複を減らすことは、**FastAPI** の中心的な考え方の1つです。

コードの重複は、バグ、セキュリティ問題、コードの同期の問題(ある場所で更新しても他の場所では更新されない場合など)の可能性を高めるためです。

これらのモデルはすべて、多くのデータを共有し、属性名と型を重複させています。

もっとうまくできるはずです。

他のモデルのベースとなる UserBase モデルを宣言することができます。そして、その属性(型宣言、検証など)を継承する、そのモデルのサブクラスを作成することができます。

すべてのデータ変換、検証、ドキュメントなどは、通常どおり機能します。

そうすれば、モデル間の違い(平文の passwordhashed_password、パスワードなし)だけを宣言すればよくなります。

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserBase(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserIn(UserBase):
    password: str


class UserOut(UserBase):
    pass


class UserInDB(UserBase):
    hashed_password: str


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserBase(BaseModel):
    username: str
    email: EmailStr
    full_name: Union[str, None] = None


class UserIn(UserBase):
    password: str


class UserOut(UserBase):
    pass


class UserInDB(UserBase):
    hashed_password: str


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved

Union または anyOf

レスポンスを2つ以上の型の Union として宣言することができます。つまり、レスポンスはそれらのいずれかになります。

OpenAPI では anyOf として定義されます。

そのためには、標準の Python 型ヒント typing.Union を使用します。

注意

Union を定義する際には、最も具体的な型を最初に記述し、その後により具体的な型を記述します。以下の例では、より具体的な PlaneItemUnion[PlaneItem, CarItem]CarItem より前に記述されています。

from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class BaseItem(BaseModel):
    description: str
    type: str


class CarItem(BaseItem):
    type: str = "car"


class PlaneItem(BaseItem):
    type: str = "plane"
    size: int


items = {
    "item1": {"description": "All my friends drive a low rider", "type": "car"},
    "item2": {
        "description": "Music is my aeroplane, it's my aeroplane",
        "type": "plane",
        "size": 5,
    },
}


@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
    return items[item_id]
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class BaseItem(BaseModel):
    description: str
    type: str


class CarItem(BaseItem):
    type: str = "car"


class PlaneItem(BaseItem):
    type: str = "plane"
    size: int


items = {
    "item1": {"description": "All my friends drive a low rider", "type": "car"},
    "item2": {
        "description": "Music is my aeroplane, it's my aeroplane",
        "type": "plane",
        "size": 5,
    },
}


@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
    return items[item_id]

Python 3.10 での Union

この例では、Union[PlaneItem, CarItem] を引数 response_model の値として渡しています。

**型アノテーション** に入れるのではなく、引数の **値として渡している** ため、Python 3.10 でも Union を使用する必要があります。

型アノテーションであれば、

some_variable: PlaneItem | CarItem

のように縦棒を使用することができました。しかし、代入 response_model=PlaneItem | CarItem にこれを入れると、Python は型アノテーションとして解釈するのではなく、PlaneItemCarItem の間で **無効な操作** を実行しようとするため、エラーが発生します。

モデルのリスト

同様に、オブジェクトのリストのレスポンスを宣言することができます。

そのためには、標準の Python の typing.List (Python 3.9 以降では list)を使用します。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str


items = [
    {"name": "Foo", "description": "There comes my hero"},
    {"name": "Red", "description": "It's my aeroplane"},
]


@app.get("/items/", response_model=list[Item])
async def read_items():
    return items
from typing import List

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str


items = [
    {"name": "Foo", "description": "There comes my hero"},
    {"name": "Red", "description": "It's my aeroplane"},
]


@app.get("/items/", response_model=List[Item])
async def read_items():
    return items

任意の dict を持つレスポンス

Pydantic モデルを使用せずに、キーと値の型だけを宣言して、プレーンな任意の dict を使用してレスポンスを宣言することもできます。

これは、有効なフィールド/属性名(Pydantic モデルに必要なもの)が事前にわからない場合に便利です。

この場合、typing.Dict (Python 3.9 以降では dict)を使用できます。

from fastapi import FastAPI

app = FastAPI()


@app.get("/keyword-weights/", response_model=dict[str, float])
async def read_keyword_weights():
    return {"foo": 2.3, "bar": 3.4}
from typing import Dict

from fastapi import FastAPI

app = FastAPI()


@app.get("/keyword-weights/", response_model=Dict[str, float])
async def read_keyword_weights():
    return {"foo": 2.3, "bar": 3.4}

まとめ

複数の Pydantic モデルを使用し、ケースごとに自由に継承します。

passwordpassword_hash、パスワードなしを含む状態を持つユーザー「エンティティ」の場合のように、エンティティごとに単一のデータモデルを持つ必要はありません。