コンテンツへスキップ

追加モデル

前の例に引き続き、複数の関連モデルを持つことはよくあることです。

これは特にユーザーモデルの場合で、次のような理由によります。

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

危険

ユーザーの平文パスワードは決して保存しないでください。常に「安全なハッシュ」を保存し、後で検証できるようにしてください。

「パスワードハッシュ」が何であるか分からない場合は、セキュリティの章で学習します。

複数のモデル

モデルがパスワードフィールドとそれらが使用される場所でどのように見えるかの一般的なアイデアを以下に示します。

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_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 モデルの内容から 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,
)

警告

補助的な追加関数 fake_password_hasherfake_save_user は、データの可能なフローをデモするためのものにすぎず、もちろん実際のセキュリティは提供していません。

重複を減らす

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

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

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

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

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

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

そうすることで、モデル間の違い (平文 password を含むもの、hashed_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

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

これは型アノテーションに含めるのではなく、引数の値として渡しているため、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 モデルを使用し、それぞれのケースで自由に継承してください。

エンティティが異なる「状態」を持つ必要がある場合、エンティティごとに単一のデータモデルを持つ必要はありません。パスワードを含む状態、ハッシュ化されたパスワードを含む状態、パスワードなしの状態を持つユーザー「エンティティ」の場合と同様です。