FastAPIのFormでNone(null)を許可すると良くない
FastAPIでフォームデータを受け取る際、パスオペレーション関数の引数に以下のように設定します。
username: str = Form()
このように定義した場合、このusername
は必須項目となります。この使い方は、公式サイトに則った正しい方法です。
では、入力が任意の項目は、どうすればいいでしょうか?Form
には第一引数にdefault値が設定できます。それをNone
に設定することにします。
この時、Form
も返値は、データが入力された場合とされなかった場合で、型が変わります。例えばフォームデータから文字列を受け取る場合は、返値の型は、str | None
となるので、以下のように設定して良さそうです。
nickname: str | None = Form(None)
しかし、こうすることでちょっと良くないことが起きます。ここでは、このことについて解説します。
何が良くないか
以下のようなパスオペレーション関数を定義したとします。
from fastapi import APIRouter, Form
router = APIRouter()
@router.post("/")
def test_form(username: str = Form(), nickname: str | None = Form(None)):
return {"username": username, "nickname": nickname}
このエンドポイントのリクエストボディの型をSwagger UIで確認してみると以下のようになります。
nicknameは、string | null
となっていることがわかると思います。
それでは、javascriptでnicknameをnull
としてリクエストを投げてみましょう。以下のコードでリクエストを投げてみました。
const formData = new FormData();
formData.append("username", "test");
formData.append("nickname", null);
axios.post("http://localhost:8001/", formData).then((r) => {
console.log(r.data);
});
typescriptだと、この時点でNG
FormData: append() メソッドの第二引数value
は、string | Blob
となっています。
ですので、以下のようにした時点でtypescriptではエラーとなります。
// Argument of type 'null' is not assignable to parameter of type 'Blob'.ts(2769)
formData.append("nickname", null);
するとコンソールに表示されたのは、以下の内容でした。
{username: 'test', nickname: 'null'}
null
は、文字列の'null'
としてレスポンスが返ってきています。FastAPIでも、nickname
は、None
ではなく文字列の'null'
となっていました。
javascriptのnull
はFastAPIでは、None
となってほしいはずです。
なぜ文字列になってしまうのか
それは、フォームデータがどのような形式で送信されているのかを確認すれば分かります。
FastAPIの開発者であれば、Swagger UIのCurlの部分を確認するとフォームデータの形式が分かりやすいです。先ほど作成したエンドポイントだと以下のようになっているはずです。
curl -X 'POST' \
'server-url' \
-H 'accept: application/json' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=string&nickname=string'
フォームデータは、'username=string&nickname=string'
という形式で送信されていることが分かります。
nicknameがnullになっている場合、フォームデータは、'username=string&nickname=null'
となってしまいます。
これでは、null
と文字列の'null'
に違いがありません。FastAPIでは、これは文字列の'null'
として認識されます。
余談:jsonではOK
リクエストボディがjsonになっている場合、null
と文字列の'null'
には以下のように明確な違いがあるので、問題ありません。
null
{
"nickname": null
}
文字列の'null'
{
"nickname": "null"
}
どうすればいいか
必須項目は、以下
username: str = Form()
任意項目は、以下
nickname: str = Form(None)
としましょう。
こうすることで、リクエストボディの型も以下のようになります。
Discussion