コンテンツにスキップ

モデル - より詳しく

先ほどの例に続き、複数の関連モデルを持つことが一般的です。

これはユーザーモデルの場合は特にそうです。なぜなら:

  • 入力モデル にはパスワードが必要です。
  • 出力モデルはパスワードをもつべきではありません。
  • データベースモデルはおそらくハッシュ化されたパスワードが必要になるでしょう。

危険

ユーザーの平文のパスワードは絶対に保存しないでください。常に認証に利用可能な「安全なハッシュ」を保存してください。

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

複数のモデル

ここでは、パスワードフィールドをもつモデルがどのように見えるのか、また、どこで使われるのか、大まかなイメージを紹介します:

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
🤓 Other versions and variants
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

**user_in.dict()について

Pydanticの.dict()

user_inUserInクラスの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モデル

上述の例ではuser_in.dict()からuser_dictをこのコードのように取得していますが:

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

これは以下と同等です:

UserInDB(**user_in.dict())

...なぜならuser_in.dict()dictであり、**を付与してUserInDBを渡してPythonに「展開」させているからです。

そこで、別のPydanticモデルのデータからPydanticモデルを取得します。

dictの展開と追加引数

そして、追加のキーワード引数hashed_password=hashed_passwordを以下のように追加すると:

UserInDB(**user_in.dict(), 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,
)

注意

サポートしている追加機能は、データの可能な流れをデモするだけであり、もちろん本当のセキュリティを提供しているわけではありません。

重複の削減

コードの重複を減らすことは、FastAPIの中核的なアイデアの1つです。

コードの重複が増えると、バグやセキュリティの問題、コードの非同期化問題(ある場所では更新しても他の場所では更新されない場合)などが発生する可能性が高くなります。

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

もっと良い方法があります。

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

データの変換、検証、文書化などはすべて通常通りに動作します。

このようにして、モデル間の違いだけを宣言することができます:

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
🤓 Other versions and variants
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

UnionまたはanyOf

レスポンスを2つの型のUnionとして宣言することができます。

OpenAPIではanyOfで定義されます。

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

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]
🤓 Other versions and variants
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のtyping.Listを使用する:

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
🤓 Other versions and variants
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を使用することができます:

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}
🤓 Other versions and variants
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 やパスワードなしなどのいくつかの「状態」をもつユーザー「エンティティ」の場合の様にすれば良いです。