追加モデル¶
前の例に引き続き、複数の関連モデルを持つことはよくあることです。
これは特にユーザーモデルの場合で、次のような理由によります。
- 入力モデルにはパスワードを含める必要があります。
- 出力モデルにはパスワードを含めるべきではありません。
- データベースモデルには、おそらくハッシュ化されたパスワードを含める必要があります。
危険
ユーザーの平文パスワードは決して保存しないでください。常に「安全なハッシュ」を保存し、後で検証できるようにしてください。
「パスワードハッシュ」が何であるか分からない場合は、セキュリティの章で学習します。
複数のモデル¶
モデルがパスワードフィールドとそれらが使用される場所でどのように見えるかの一般的なアイデアを以下に示します。
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_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_hasher
と fake_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
を定義するときは、最も具体的な型を最初に含め、次に具体的な型を含めます。以下の例では、より具体的な PlaneItem
が Union[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 はそれを型アノテーションとして解釈する代わりに、PlaneItem
と CarItem
の間で無効な操作を実行しようとするからです。
モデルのリスト¶
同様に、オブジェクトのリストのレスポンスを宣言できます。
そのためには、標準的な 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 モデルを使用し、それぞれのケースで自由に継承してください。
エンティティが異なる「状態」を持つ必要がある場合、エンティティごとに単一のデータモデルを持つ必要はありません。パスワードを含む状態、ハッシュ化されたパスワードを含む状態、パスワードなしの状態を持つユーザー「エンティティ」の場合と同様です。