Skip to content

Conversation

@YuriiMotov
Copy link
Member

@YuriiMotov YuriiMotov commented Nov 2, 2025

Credits

The author of solution is @sneakers-the-rat. Initial PR: #13464.
I just separated these changes from (IMO) unnecessary\unrelated changes, refactored the code a bit and simplified tests to make it easier to review.

Problem description

Current implementation of _get_multidict_value puts the default value if the parameter value is empty string or is missing.
This makes it impossible to determine if this value was passed explicitly or not.

See code example in the details
from typing import Annotated

from fastapi import FastAPI, Form
from fastapi.testclient import TestClient
from pydantic import BaseModel


class ExampleModel(BaseModel):
    field_1: bool = True

app = FastAPI()

@app.post("/form")
async def form_endpoint(model: Annotated[ExampleModel, Form()]):
    return {"fields_set": list(model.model_fields_set)}

client = TestClient(app)


def test_form():
    resp = client.post("/form", data={})
    assert resp.status_code == 200, resp.text
    fields_set = resp.json()["fields_set"]
    assert fields_set == []  # AssertionError: assert ['field_1'] == []

Also, there is a bug that treating empty strings in input as None doesn't work if the default value is None:

@app.post("/")
def read_root(user_id: Annotated[int | None, Form()] = None):
    return user_id
See test in the details
from typing import Annotated

from fastapi import FastAPI, Form
from fastapi.testclient import TestClient

app = FastAPI()

@app.post("/")
def read_root(
    user_id: Annotated[int | None, Form()] = None,
):
    return user_id

def test_pass_empty_str():
    client = TestClient(app)
    response = client.post("/", data={"user_id": ""})
    assert response.status_code == 200, response.text
    # ... unable to parse string as an integer","input":""

    assert response.json() is None

Current ststus

Waiting for the #13537 to be merged. It will simplify this PR a bit

Related discussions and PRs

Some clarifications

Why if key not in values: changed to if key not in field_aliases:?

In _extract_form_body we now pass form_input=True to _get_multidict_value that disables the pre-filling with default values the form parameters with missing or empty str values.

Later in _extract_form_body there were lines that added extra parameters to values.

    for key, value in received_body.items():
        if key not in values:
            values[key] = value
    return values

It worked previously because we had values for all explicitly declared parameters (we used defaults if parameter was not passed or passed as "").
But now it's not true, so we need to update this condition to if key not in field_aliases:. Otherwise it would break the logic of treating empty str as absence of the value for Form parameters.

Why isinstance(field.field_info, (params.Form, temp_pydantic_v1_params.Form)) was removed?

This was the way to distinguish Form parameters from others. But now we have form_input parameter that effectively does it.

Also, as we previously only checked Form class, the logic of treating empty str as None didn't work for File parameters. So, "Send empty value" option in Swagger didn't work for File parameters.
Now it will work.

We can consider moving this to separate PR which should be merged before this one.

Tests

Part of tests are added just for completeness to cover different ways of Form field declaration.

On master the following tests fail:

  • test_defaults_form_param[empty_strings_sent] - empty string to File parameter is not treated as None
  • test_defaults_form_model[empty_strings_sent] - same as previous but with parameter declared as form model
  • test_form_model_fields_set - currently FastAPI pre-fills values with defaults, so model_fields_set doesn't work as expected

sneakers-the-rat and others added 7 commits October 30, 2025 17:34
commit 1b8d0d7
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Wed Aug 20 03:24:25 2025 -0700

    ok but seriously

commit d3ccab4
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Tue Aug 19 23:38:25 2025 -0700

    rm being able to determine the input format of a model

commit fec0a06
Merge: 3f2e0f5 cad08bb
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Mon Apr 14 20:03:14 2025 -0700

    Merge branch 'form-defaults' of https://github.com/sneakers-the-rat/fastapi into form-defaults

commit 3f2e0f5
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Mon Apr 14 20:01:50 2025 -0700

    lint

commit cad08bb
Author: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date:   Tue Apr 15 02:47:42 2025 +0000

    🎨 [pre-commit.ci] Auto format from pre-commit.com hooks

commit f63e983
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Mon Apr 14 19:46:34 2025 -0700

    docs for handling default values, pass field to validation context

commit 529d486
Merge: a9acab8 159824e
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Sat Mar 8 17:45:40 2025 -0800

    Merge branch 'form-defaults' of https://github.com/sneakers-the-rat/fastapi into form-defaults

commit a9acab8
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Sat Mar 8 17:42:38 2025 -0800

    lint

commit 159824e
Author: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date:   Sun Mar 9 01:42:31 2025 +0000

    🎨 [pre-commit.ci] Auto format from pre-commit.com hooks

commit e761843
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Sat Mar 8 17:42:21 2025 -0800

    pydantic v1 compat

commit 64f2528
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Sat Mar 8 17:38:03 2025 -0800

    fix handling form data with fields that are not annotated as Form()

commit 7fade13
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Sat Mar 8 17:37:32 2025 -0800

    fix just the extra values problem (again, purposefully with failing tests to demonstrate the problem, fixing in next commit)

commit 49f6b83
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Sat Mar 8 17:36:03 2025 -0800

    add failing tests for empty input values to get a CI run baseline for them

commit 15eb678
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Thu Mar 6 19:35:53 2025 -0800

    mypy lint

commit 1a58af4
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Thu Mar 6 19:31:28 2025 -0800

    finish pydantic 1 compat

commit a2ad8b1
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Thu Mar 6 19:22:11 2025 -0800

    python 3.8 and pydantic 1 compat

commit 76c4d31
Author: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Date:   Thu Mar 6 19:06:44 2025 -0800

    don't prefill defaults in form input
@YuriiMotov YuriiMotov added the bug Something isn't working label Nov 2, 2025
@YuriiMotov YuriiMotov force-pushed the issue-13399_dont-prefill-form branch from 5a0ee8d to 4c68df1 Compare November 2, 2025 09:52
@sneakers-the-rat

This comment was marked as resolved.

@YuriiMotov

This comment was marked as resolved.

@sneakers-the-rat

This comment was marked as resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants