🤔

FastAPIのFormでNone(null)を許可すると良くない

2024/07/10に公開

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