本文へスキップ
タンヴィル・アハメド

Python・バックエンド

FastAPI 0.150へのアップグレード:空のファイルアップロードと最近の変更点

FastAPI 0.150.0では、ブラウザーで未選択のファイル入力が未指定として扱われます。ファイルアップロードのエンドポイント、フォームモデル、テストの更新方法を解説します。

著者:タンヴィル・アハメド・公開 ・約25分で読めます

AIの支援を受けて調査・執筆した記事です。出典へのリンクは以下に掲載しています。

Read this article in English

ユーザーがファイル入力でファイルを選択せずにHTMLフォームを送信すると、ブラウザーは空のファイル名と空のコンテンツを持つマルチパートフォームパートを送信します(FastAPI 0.150.0 release notes)。FastAPI 0.150.0より前は、この空のパートがfilename=""のUploadFileオブジェクトまたは空のbytes(b"")としてパースされていたため、ファイルが実際にアップロードされたかどうかを判別するにはエンドポイント側で手動でファイル名を検査する必要がありました(FastAPI 0.150.0 release notes)。FastAPI 0.150.0では、ファイル名とコンテンツの両方が空であるマルチパートのファイルパートを未指定として扱うことで、この問題が修正されています(FastAPI 0.150.0 release notes)。このガイドでは、ファイルアップロードのエンドポイントから従来のファイル名チェックを削除する手順、FastAPI 0.147.0から0.149.0までの関連するフォームおよびパラメーターの変更点の確認、そしてTestClientテストスイートの更新方法について解説します。

なぜ0.150.0より前はブラウザーのファイル入力でバリデーションの回避策が必要だったのか

enctype="multipart/form-data"を使用する標準的なHTMLフォームでは、<input type="file" name="file">要素がユーザーによって空のままにされても、そのフィールド自体は省略されません(FastAPI 0.150.0 release notes)。代わりに、ブラウザーはfilename属性が""でボディが0バイト(b"")のマルチパートセグメントを送信します(FastAPI 0.150.0 release notes)。

0.150.0より前のFastAPIは、この空のセグメントをそのまま有効なパラメーター値に変換していました(FastAPI 0.150.0 release notes)。そのため、バックエンドのハンドラーには次の2つの問題が生じていました。

  1. 必須のファイルパラメーター(file: UploadFileやfile: Annotated[bytes, File()])において、ユーザーが一度もファイルを選択していなくても、FastAPIはフィールドの欠落ではなくUploadFile(filename="")インスタンスやb""として認識するため、リクエストのバリデーションを通過してしまっていました(FastAPI 0.150.0 release notes)。
  2. 任意のファイルパラメーター(file: UploadFile | None = None)において、ブラウザーのフォームから送信された際にNoneではなく空のファイル名を持つUploadFileオブジェクトを受け取ってしまっていました(FastAPI 0.150.0 release notes)。

空のアップロードがストレージやパーサーのロジックに到達するのを防ぐため、エンドポイントではif not file.filename:をチェックし、手動でHTTPエラーを送出するかオブジェクトが存在しないものとして扱う必要がありました(FastAPI 0.150.0 release notes、FastAPI Request Files)。なお、FastAPIでフォームやアップロードされたファイルを扱うにはpython-multipart(uv add python-multipartまたはpip install python-multipart)が必要です(FastAPI Request Files)。

FastAPI 0.150.0より前の防御的なエンドポイントは次のようになっていました。

from fastapi import FastAPI, HTTPException, UploadFile
 
app = FastAPI()
 
 
@app.post("/upload-required/")
async def upload_required_legacy(file: UploadFile):
    if not file.filename:
        raise HTTPException(status_code=422, detail="File was not selected")
    content = await file.read()
    return {"filename": file.filename, "size": len(content)}
 
 
@app.post("/upload-optional/")
async def upload_optional_legacy(file: UploadFile | None = None):
    if file is None or not file.filename:
        return {"message": "No upload file sent"}
    return {"filename": file.filename}

FastAPI 0.150.0での空のファイルパートの扱い

2026年10月にリリースされたFastAPI 0.150.0では、空のファイル名(filename="")と空のコンテンツ(b"")の両方を持つマルチパートのファイルパートはすべて未指定として扱われます(FastAPI 0.150.0 release notes)。この変更は、4つの異なるパラメーター設定に影響します(FastAPI 0.150.0 release notes)。

  • 必須のファイルフィールド: file: UploadFileまたはfile: Annotated[bytes, File()]を宣言しているエンドポイントは、ブラウザーの未選択のファイル入力を422 Unprocessable Entityバリデーションエラーで自動的に拒否するようになりました(FastAPI 0.150.0 release notes)。
  • 任意のファイルフィールドとデフォルト値: file: UploadFile | None = Noneを宣言しているエンドポイント、あるいはFile(...)やPydanticフォームモデル内でdefaultやdefault_factoryが設定されているフィールドは、ファイル入力が未選択のまま送信された場合に設定済みのデフォルト値を受け取ります(FastAPI 0.150.0 release notes)。フォームモデルでは、デフォルト値が適用されてもフィールドが明示的に設定されたとは見なされないため、model_dump(exclude_unset=True)を呼び出すとそのフィールドは除外されます(FastAPI Release Notes)。
  • ファイルのリスト: files: list[UploadFile]やfiles: Annotated[list[bytes], File()]を受け取る場合、リスト内の未選択のファイル入力は、UploadFile(filename="")やb""を含む単一要素のリストを生成するのではなく、無視されて除外されます(FastAPI 0.150.0 release notes)。必須のlist[UploadFile]が未選択の入力のみを受け取った場合に空のリスト[]に解決されるか、それとも422バリデーションエラーを返すかについては、リリースノートでは確認されていません。
  • 名前付きの0バイトファイル: ユーザーが0バイトを含むempty.txt(filename="empty.txt"、コンテンツb"")のようなファイル名を持つ実際の空ファイルをアップロードした場合、ファイル名が空ではないため、FastAPIはそれを有効なアップロードとして保持します(FastAPI 0.150.0 release notes)。

単一および任意のファイルアップロードエンドポイントの更新

FastAPI 0.150.0では、未選択の入力に対する手動のfile.filenameチェックを削除し、標準の型アノテーションに任せることができます(FastAPI 0.150.0 release notes、FastAPI Request Files)。次の例は、bytesとUploadFileの両方のパラメーターに対応する更新後のハンドラーを示しています(FastAPI Request Files、UploadFile reference)。

from typing import Annotated
from fastapi import FastAPI, File, UploadFile
 
app = FastAPI()
 
 
@app.post("/files/required/")
async def create_required_file(file: UploadFile):
    content = await file.read()
    return {"filename": file.filename, "size": len(content)}
 
 
@app.post("/files/optional-bytes/")
async def create_optional_file(
    file: Annotated[bytes | None, File()] = None,
):
    if file is None:
        return {"message": "No file sent"}
    return {"file_size": len(file)}
 
 
@app.post("/uploadfile/optional/")
async def create_optional_upload_file(
    file: UploadFile | None = None,
):
    if file is None:
        return {"message": "No upload file sent"}
    return {"filename": file.filename}

create_optional_file内のif file is None:というチェックに注目してください。FastAPIの公式チュートリアルでは、任意のbytesパラメーターに対してしばしばif not file:が使われています(FastAPI Request Files)。しかしPythonでは、b""は偽(falsy)と評価されます。もし送信側が名前付きの0バイトファイル(filename="empty.txt"、コンテンツb"")をアップロードした場合、FastAPI 0.150.0はそれを保持し、file: Annotated[bytes | None, File()] = Noneにb""を渡します(FastAPI 0.150.0 release notes)。if file is None:でチェックすることで、省略または未選択のファイル(None)と名前付きの0バイトファイル(b"")を区別できます。

Pydanticフォームモデル(FastAPI 0.113.0以降でサポート)を使用してマルチパートフィールドをグループ化している場合も、任意のファイルフィールドがブラウザーのフォームで未選択のままにされたときはmodel_dump(exclude_unset=True)から除外されたままになります(FastAPI Form Models、FastAPI Release Notes、FastAPI 0.150.0 release notes)。

複数ファイルアップロードと0バイトのエッジケースの処理

HTMLフォームに<input type="file" name="files" multiple>や同じ名前を共有する複数のファイル入力が含まれている場合も、ユーザーがファイルを選択せずにフォームを送信するとブラウザーは空のパートを送信します(FastAPI 0.150.0 release notes、FastAPI Request Files)。FastAPI 0.150.0では、list[UploadFile]またはAnnotated[list[bytes], File()]内の未選択の入力は除外されます(FastAPI 0.150.0 release notes)。

つまり、アップロードされたファイルをループ処理する際に、upload.filename == ""の項目をスキップする必要はなくなります(FastAPI 0.150.0 release notes)。

from fastapi import FastAPI, UploadFile
 
app = FastAPI()
 
 
@app.post("/batch-upload/")
async def upload_multiple_files(files: list[UploadFile]):
    processed = []
    for upload in files:
        data = await upload.read()
        processed.append({
            "filename": upload.filename,
            "size": len(data),
        })
    return {"files": processed}

送信側が未選択の入力パートとともに2つの実際のファイル(placeholder.txtという名前の0バイトファイルを含む)を送信した場合、FastAPIは未選択のパートを除外し、名前付きの2つのUploadFileインスタンスのみをupload_multiple_filesに渡します(FastAPI 0.150.0 release notes)。

FastAPI 0.147.0から0.149.0までのその他の変更点

fastapi run --root-pathへの移行に伴いFastAPI()のroot_pathパラメーターを非推奨としたFastAPI 0.146.0、またはFastAPI()から非推奨のopenapi_prefixおよび未使用の**extraキーワード引数を削除した0.145.0からアップグレードする場合、2026年10月19日に公開された3つの中間リリースでもフォームとデフォルト値のバリデーション方法が変更されています(FastAPI 0.146.0 release notes、FastAPI Release Notes)。一度にバージョンを引き上げる際に想定外の挙動を防ぐため、これらもあわせて確認しておきます。

FastAPI 0.147.0: 省略された真偽値フォームフィールドのデフォルトがFalseに

標準的なHTMLフォームでは、チェックされていない<input type="checkbox">要素は送信されるペイロードから完全に省略されます(FastAPI 0.147.0 release notes、FastAPI Request Forms)。FastAPI 0.147.0では、省略されたboolおよびbool | NoneのフォームフィールドをFalseとして解釈します(FastAPI 0.147.0 release notes)。

  • この変更は、フィールドが必須であるかデフォルト値としてTrueやNoneを定義しているかにかかわらず、単体のFormパラメーターとPydanticフォームモデル(Annotated[MyFormModel, Form()])の両方に適用されます(FastAPI 0.147.0 release notes)。
  • 推論されたFalseの値は明示的に設定されたものとして扱われるため、フォームモデルでmodel_dump(exclude_unset=True)を呼び出すとこの真偽値フィールドが含まれます(FastAPI 0.147.0 release notes)。
  • JSONリクエストボディおよび直接のPydanticバリデーションは影響を受けません(FastAPI 0.147.0 release notes)。

FastAPI 0.148.0: パラメーターおよびパラメーターモデルのデフォルト値の扱い

FastAPI 0.148.0では、省略されたQuery、Header、Cookie、Formパラメーター(およびパラメーターモデルのフィールド)のデフォルト値を生のユーザー入力として扱うのをやめました(FastAPI 0.148.0 release notes)。

  • モデルフィールドでのField(validate_default=True)やパラメーターでのQuery(validate_default=True)のように、validate_default=Trueで明示的にオプトインしない限り、デフォルト値はPydanticバリデーターを通過しなくなりました(FastAPI 0.148.0 release notes)。Body(validate_default=True)も同様に尊重されるようになっています(FastAPI 0.148.0 release notes)。
  • パラメーターモデル内の省略されたフィールドにデフォルト値を適用しても、そのフィールドが設定済みとしてマークされなくなり、model_dump(exclude_unset=True)からは除外されるようになりました(FastAPI 0.148.0 release notes)。ただし、省略された真偽値のフォームフィールドは例外のままであり、引き続き明示的にFalseに設定されたものとして扱われます(FastAPI 0.148.0 release notes)。
  • default_factoryは省略されたフィールドごとに1回実行されるようになり、バリデーションエラーのレスポンスではエラーのinputフィールド内に注入されたデフォルト値が漏洩しなくなりました(FastAPI 0.148.0 release notes)。

FastAPI 0.149.0: フォームモデル内の空のスカラー値を未指定として扱う

ブラウザーは空のテキスト入力(<input type="text">)を空文字列("")として送信します(FastAPI 0.149.0 release notes)。FastAPI 0.149.0では、Pydanticフォームモデル内の空のスカラー値("")を未指定として扱い、フォームモデルの挙動を単体のFormパラメーターと揃えています(FastAPI 0.149.0 release notes)。

  • フォームモデル内の必須のスカラーフィールドは、""として送信されると422でバリデーションに失敗するようになりました(FastAPI 0.149.0 release notes)。
  • フォームモデル内の任意のスカラーフィールドは、""として送信されるとdefaultまたはdefault_factoryにフォールバックし、model_dump(exclude_unset=True)では未設定のままになります(FastAPI 0.149.0 release notes)。
  • リストフィールド(list[str])内の空文字列は保持されます(FastAPI 0.149.0 release notes)。

0.147.0から0.150.0までのルールの下で、Pydanticフォームモデルに任意のUploadFileと真偽値のチェックボックスを組み合わせた完全な例は次のとおりです(FastAPI Form Models、FastAPI Release Notes、FastAPI 0.150.0 release notes)。

from typing import Annotated
from fastapi import FastAPI, Form, UploadFile
from pydantic import BaseModel
 
app = FastAPI()
 
 
class ProfileForm(BaseModel):
    username: str
    bio: str = "No bio provided"
    subscribe_newsletter: bool = True
    avatar: UploadFile | None = None
 
 
@app.post("/profile/")
async def update_profile(form: Annotated[ProfileForm, Form()]):
    unset_excluded = form.model_dump(exclude_unset=True, exclude={"avatar"})
    return {
        "username": form.username,
        "bio": form.bio,
        "subscribe_newsletter": form.subscribe_newsletter,
        "has_avatar": form.avatar is not None,
        "explicitly_set_fields": list(unset_excluded.keys()),
    }

ブラウザーがusername="alice"、bio=""を送信し、subscribe_newsletterチェックボックスを省略し、avatarファイル入力を未選択のままにした場合、結果は次のようになります。

  • usernameは"alice"になります。
  • bioは"No bio provided"にフォールバックし、explicitly_set_fieldsから除外されます(0.148.0および0.149.0)(FastAPI 0.148.0 release notes、FastAPI 0.149.0 release notes)。
  • subscribe_newsletterはFalseになり(省略されたチェックボックスは未チェックを意味するため、Trueのデフォルトを上書きします)、explicitly_set_fieldsに含まれます(0.147.0)(FastAPI 0.147.0 release notes)。
  • avatarはNoneになり、model_dump(exclude_unset=True)から除外されます(0.150.0)(FastAPI 0.150.0 release notes)。

0.150.0に向けたテストスイートの更新

FastAPIのTestClientを使用してファイルアップロードのエンドポイントをテストする場合(FastAPI Testing、TestClient reference)、files={"file": ("", b"")}を介して渡されるマルチパートファイルのタプルは、0.150.0へのアップグレード後に挙動が変わる可能性があります(FastAPI 0.150.0 release notes)。

  1. 未選択のブラウザー入力のシミュレーション: 必須のUploadFileエンドポイントにfiles={"file": ("", b"")}を送信すると、FastAPIのバリデーション層から422 Unprocessable Entityが返されるようになりました(FastAPI 0.150.0 release notes)。テストが以前、独自のハンドラーによって送出された200 OKやカスタムの400 Bad Requestをアサートしていた場合は、422を期待するようにアサーションを更新してください(FastAPI 0.150.0 release notes)。
  2. 意図的な0バイトファイルのアップロードのテスト: エンドポイントが空ファイルをどう処理するかをテストするための省略記法としてfiles={"file": ("", b"")}を使用していた場合、FastAPIはそれを未選択の入力として拒否するようになります(FastAPI 0.150.0 release notes)。FastAPIが名前付きの0バイトファイルとして認識できるよう、files={"file": ("empty.txt", b"")}のようにタプル内に空ではないファイル名を指定してください(FastAPI 0.150.0 release notes)。

TestClientを使用して3つのケースすべてを検証する実行可能なテストスイートは次のとおりです(FastAPI Testing、FastAPI 0.150.0 release notes)。

from fastapi import FastAPI, UploadFile
from fastapi.testclient import TestClient
 
app = FastAPI()
 
 
@app.post("/upload/")
async def upload_file(file: UploadFile):
    content = await file.read()
    return {"filename": file.filename, "size": len(content)}
 
 
@app.post("/upload-optional/")
async def upload_optional(file: UploadFile | None = None):
    if file is None:
        return {"uploaded": False}
    content = await file.read()
    return {"uploaded": True, "filename": file.filename, "size": len(content)}
 
 
client = TestClient(app)
 
 
def test_unselected_file_on_required_endpoint_returns_422():
    response = client.post("/upload/", files={"file": ("", b"")})
    assert response.status_code == 422
 
 
def test_named_zero_byte_file_succeeds():
    response = client.post("/upload/", files={"file": ("empty.txt", b"")})
    assert response.status_code == 200
    assert response.json() == {"filename": "empty.txt", "size": 0}
 
 
def test_unselected_file_on_optional_endpoint_resolves_to_none():
    response = client.post("/upload-optional/", files={"file": ("", b"")})
    assert response.status_code == 200
    assert response.json() == {"uploaded": False}

FastAPI 0.150.0へのアップグレードを完了するには、コードベース内のルートハンドラーにある.filenameのチェックやテストファイル内の("", b"")タプルを検索し、手動の回避策を削除した上で、更新されたバリデーションルールに対してテストスイートを実行してください。

参考資料

  • FastAPI
  • Python
  • API
  • バックエンド