追加モデル¶
前の例に引き続き、複数の関連モデルを持つことは一般的です。
これは特にユーザーモデルの場合に当てはまります。なぜなら
- **入力モデル**はパスワードを持つことができる必要があるためです。
- **出力モデル**はパスワードを持つべきではないためです。
- **データベース モデル**はおそらくハッシュ化されたパスワードを持つ必要があるためです。
複数のモデル¶
パスワードフィールドとその使用場所を持つモデルの一般的な例を次に示します。
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_dict
を user_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_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
¶
この例では、Union[PlaneItem, CarItem]
を引数 response_model
の値として渡しています。
**型アノテーション** に入れるのではなく、引数の **値として渡している** ため、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 モデルを使用し、ケースごとに自由に継承します。
password
、password_hash
、パスワードなしを含む状態を持つユーザー「エンティティ」の場合のように、エンティティごとに単一のデータモデルを持つ必要はありません。