Browse Source
Fixes #13399 **Problem:** When using Form-based dependency models, default values were being pre-filled during form parsing, causing `model_fields_set` to include all fields with defaults even when they weren't explicitly provided in the request. This made it impossible to distinguish between explicitly set fields and fields using their default values. JSON body models didn't have this issue. **Root Cause:** In `_get_multidict_value()`, when a form field value was `None` (not provided) and the field wasn't required, the function returned `deepcopy(field.default)`. This pre-filled default was then added to `params_to_process` and passed to Pydantic, which treated it as explicitly set. **Solution:** Changed `_get_multidict_value()` to return `None` instead of the default value when a field is not provided. This allows Pydantic to handle defaults during validation, correctly preserving `model_fields_set` to only include fields that were actually provided in the form data. **Testing:** - Added comprehensive test suite comparing JSON and Form behavior - Verified that Form models now match JSON model behavior for `model_fields_set` - All existing tests continue to pass This makes Form and JSON body parameter handling consistent, allowing developers to use `model_fields_set`, `model_dump(exclude_unset=True)`, and `model_dump(exclude_defaults=True)` correctly with Form-based models.pull/14870/head
2 changed files with 123 additions and 4 deletions
@ -0,0 +1,120 @@ |
|||||
|
""" |
||||
|
Test that Form-based dependency models preserve model_fields_set correctly. |
||||
|
|
||||
|
This test addresses issue https://github.com/fastapi/fastapi/issues/13399 |
||||
|
where Form models were pre-filling default values, making it impossible |
||||
|
to distinguish between explicitly set fields and fields using defaults. |
||||
|
""" |
||||
|
|
||||
|
from typing import Annotated |
||||
|
|
||||
|
import pytest |
||||
|
from fastapi import FastAPI, Form |
||||
|
from fastapi.testclient import TestClient |
||||
|
from pydantic import BaseModel |
||||
|
|
||||
|
|
||||
|
class ModelWithDefaults(BaseModel): |
||||
|
field_bool: bool = True |
||||
|
field_str: str = "default_value" |
||||
|
field_int: int = 42 |
||||
|
|
||||
|
|
||||
|
app = FastAPI() |
||||
|
|
||||
|
|
||||
|
@app.post("/json") |
||||
|
async def json_endpoint(model: ModelWithDefaults): |
||||
|
return { |
||||
|
"fields_set": list(model.model_fields_set), |
||||
|
"field_bool": model.field_bool, |
||||
|
"field_str": model.field_str, |
||||
|
"field_int": model.field_int, |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@app.post("/form") |
||||
|
async def form_endpoint(model: Annotated[ModelWithDefaults, Form()]): |
||||
|
return { |
||||
|
"fields_set": list(model.model_fields_set), |
||||
|
"field_bool": model.field_bool, |
||||
|
"field_str": model.field_str, |
||||
|
"field_int": model.field_int, |
||||
|
} |
||||
|
|
||||
|
|
||||
|
client = TestClient(app) |
||||
|
|
||||
|
|
||||
|
def test_json_empty_preserves_fields_set(): |
||||
|
"""JSON with empty dict should have empty fields_set""" |
||||
|
response = client.post("/json", json={}) |
||||
|
assert response.status_code == 200 |
||||
|
data = response.json() |
||||
|
assert data["fields_set"] == [] |
||||
|
assert data["field_bool"] is True |
||||
|
assert data["field_str"] == "default_value" |
||||
|
assert data["field_int"] == 42 |
||||
|
|
||||
|
|
||||
|
def test_json_partial_preserves_fields_set(): |
||||
|
"""JSON with partial data should only set provided fields""" |
||||
|
response = client.post("/json", json={"field_str": "custom"}) |
||||
|
assert response.status_code == 200 |
||||
|
data = response.json() |
||||
|
assert data["fields_set"] == ["field_str"] |
||||
|
assert data["field_bool"] is True |
||||
|
assert data["field_str"] == "custom" |
||||
|
assert data["field_int"] == 42 |
||||
|
|
||||
|
|
||||
|
def test_json_all_fields_preserves_fields_set(): |
||||
|
"""JSON with all fields should mark all as set""" |
||||
|
response = client.post( |
||||
|
"/json", |
||||
|
json={"field_bool": False, "field_str": "custom", "field_int": 100}, |
||||
|
) |
||||
|
assert response.status_code == 200 |
||||
|
data = response.json() |
||||
|
assert set(data["fields_set"]) == {"field_bool", "field_str", "field_int"} |
||||
|
assert data["field_bool"] is False |
||||
|
assert data["field_str"] == "custom" |
||||
|
assert data["field_int"] == 100 |
||||
|
|
||||
|
|
||||
|
def test_form_empty_preserves_fields_set(): |
||||
|
"""Form with empty data should have empty fields_set (matching JSON behavior)""" |
||||
|
response = client.post("/form", data={}) |
||||
|
assert response.status_code == 200 |
||||
|
data = response.json() |
||||
|
# This is the key fix: Form should behave like JSON |
||||
|
assert data["fields_set"] == [] |
||||
|
assert data["field_bool"] is True |
||||
|
assert data["field_str"] == "default_value" |
||||
|
assert data["field_int"] == 42 |
||||
|
|
||||
|
|
||||
|
def test_form_partial_preserves_fields_set(): |
||||
|
"""Form with partial data should only set provided fields (matching JSON behavior)""" |
||||
|
response = client.post("/form", data={"field_str": "custom"}) |
||||
|
assert response.status_code == 200 |
||||
|
data = response.json() |
||||
|
# This is the key fix: only field_str should be in fields_set |
||||
|
assert data["fields_set"] == ["field_str"] |
||||
|
assert data["field_bool"] is True |
||||
|
assert data["field_str"] == "custom" |
||||
|
assert data["field_int"] == 42 |
||||
|
|
||||
|
|
||||
|
def test_form_all_fields_preserves_fields_set(): |
||||
|
"""Form with all fields should mark all as set""" |
||||
|
response = client.post( |
||||
|
"/form", |
||||
|
data={"field_bool": "false", "field_str": "custom", "field_int": "100"}, |
||||
|
) |
||||
|
assert response.status_code == 200 |
||||
|
data = response.json() |
||||
|
assert set(data["fields_set"]) == {"field_bool", "field_str", "field_int"} |
||||
|
assert data["field_bool"] is False |
||||
|
assert data["field_str"] == "custom" |
||||
|
assert data["field_int"] == 100 |
||||
Loading…
Reference in new issue