Extra Models¶
🌐 Translation by AI and humans
This translation was made by AI guided by humans. 🤝
It could have mistakes of misunderstanding the original meaning, or looking unnatural, etc. 🤖
You can improve this translation by helping us guide the AI LLM better.
先ほどの例に続き、複数の関連モデルを持つことは一般的です。
これはユーザーモデルの場合は特にそうです。なぜなら:
- 入力モデル にはパスワードが必要です。
- 出力モデルはパスワードをもつべきではありません。
- データベースモデルはおそらくハッシュ化されたパスワードが必要になるでしょう。
Danger
ユーザーの平文のパスワードは絶対に保存しないでください。常に検証できる「安全なハッシュ」を保存してください。
知らない方は、セキュリティの章で「パスワードハッシュ」とは何かを学ぶことができます。
Multiple models¶
ここでは、パスワードフィールドをもつモデルがどのように見えるのか、また、どこで使われるのか、大まかなイメージを紹介します:
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.model_dump(), 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 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.model_dump(), 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
About **user_in.model_dump()¶
Pydanticの.model_dump()¶
user_inはUserInクラスのPydanticモデルです。
Pydanticモデルには、モデルのデータを含むdictを返す.model_dump()メソッドがあります。
そこで、以下のようなPydanticオブジェクトuser_inを作成すると:
user_in = UserIn(username="john", password="secret", email="john.doe@example.com")
そして呼び出すと:
user_dict = user_in.model_dump()
これで変数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.model_dump()からuser_dictをこのコードのように取得していますが:
user_dict = user_in.model_dump()
UserInDB(**user_dict)
これは以下と同等です:
UserInDB(**user_in.model_dump())
...なぜならuser_in.model_dump()はdictであり、**を付与してUserInDBを渡してPythonに「展開」させているからです。
そこで、別のPydanticモデルのデータからPydanticモデルを取得します。
dictの展開と追加キーワード¶
そして、追加のキーワード引数hashed_password=hashed_passwordを以下のように追加すると:
UserInDB(**user_in.model_dump(), 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,
)
Warning
追加のサポート関数fake_password_hasherとfake_save_userは、データの可能な流れをデモするだけであり、もちろん本当のセキュリティを提供しているわけではありません。
Reduce duplication¶
コードの重複を減らすことは、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.model_dump(), 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 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.model_dump(), 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 or 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]
🤓 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 3.10のUnion¶
この例では、引数response_modelの値としてUnion[PlaneItem, CarItem]を渡しています。
型アノテーションに書くのではなく、引数の値として渡しているため、Python 3.10でもUnionを使う必要があります。
型アノテーションであれば、次のように縦棒を使用できました:
some_variable: PlaneItem | CarItem
しかし、これを代入でresponse_model=PlaneItem | CarItemのように書くと、Pythonはそれを型アノテーションとして解釈するのではなく、PlaneItemとCarItemの間で無効な操作を行おうとしてしまうため、エラーになります。
List of models¶
同じように、オブジェクトのリストのレスポンスを宣言できます。
そのためには、標準の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
Response with arbitrary 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}
Recap¶
複数のPydanticモデルを使用し、ケースごとに自由に継承します。
エンティティが異なる「状態」を持たなければならない場合は、エンティティごとに単一のデータモデルを持つ必要はありません。password、password_hash、パスワードなしを含む状態を持つユーザー「エンティティ」の場合と同様です。