mirror of https://github.com/tiangolo/fastapi.git
Fix Optional Literal Form field handling when value is None
This commit is contained in:
parent
597b435ae7
commit
dd7807ed0c
|
|
@ -347,6 +347,21 @@ def add_non_field_param_to_dependency(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_optional_type(annotation: Any) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a type annotation is Optional (Union with None).
|
||||||
|
|
||||||
|
Optional[X] is equivalent to Union[X, None], so we check if:
|
||||||
|
1. The origin is Union
|
||||||
|
2. NoneType is one of the union args
|
||||||
|
"""
|
||||||
|
origin = get_origin(annotation)
|
||||||
|
if origin is Union:
|
||||||
|
args = get_args(annotation)
|
||||||
|
return type(None) in args
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ParamDetails:
|
class ParamDetails:
|
||||||
type_annotation: Any
|
type_annotation: Any
|
||||||
|
|
@ -412,7 +427,12 @@ def analyze_param(
|
||||||
assert not is_path_param, "Path parameters cannot have default values"
|
assert not is_path_param, "Path parameters cannot have default values"
|
||||||
field_info.default = value
|
field_info.default = value
|
||||||
else:
|
else:
|
||||||
field_info.default = RequiredParam
|
# Check if the type annotation is Optional (Union with None)
|
||||||
|
# If so, use None as default instead of RequiredParam
|
||||||
|
if _is_optional_type(type_annotation):
|
||||||
|
field_info.default = None
|
||||||
|
else:
|
||||||
|
field_info.default = RequiredParam
|
||||||
# Get Annotated Depends
|
# Get Annotated Depends
|
||||||
elif isinstance(fastapi_annotation, params.Depends):
|
elif isinstance(fastapi_annotation, params.Depends):
|
||||||
depends = fastapi_annotation
|
depends = fastapi_annotation
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
from typing import Annotated, Literal, Optional
|
||||||
|
from fastapi import FastAPI, Form
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
||||||
|
def test_optional_literal_form_none():
|
||||||
|
"""Test that omitting an optional form field returns None"""
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.post("/")
|
||||||
|
async def read_main(
|
||||||
|
attribute: Annotated[Optional[Literal["abc", "def"]], Form()]
|
||||||
|
):
|
||||||
|
return {"attribute": attribute}
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
# FIXED: Use empty data instead of {"attribute": None}
|
||||||
|
response = client.post("/", data={})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"attribute": None}
|
||||||
|
|
||||||
|
|
||||||
|
def test_optional_literal_form_valid_values():
|
||||||
|
"""Test that valid literal values work correctly"""
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.post("/")
|
||||||
|
async def read_main(
|
||||||
|
attribute: Annotated[Optional[Literal["abc", "def"]], Form()]
|
||||||
|
):
|
||||||
|
return {"attribute": attribute}
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
# Test with "abc"
|
||||||
|
response = client.post("/", data={"attribute": "abc"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"attribute": "abc"}
|
||||||
|
|
||||||
|
# Test with "def"
|
||||||
|
response = client.post("/", data={"attribute": "def"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"attribute": "def"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_optional_literal_form_invalid_value():
|
||||||
|
"""Test that invalid values return 422"""
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.post("/")
|
||||||
|
async def read_main(
|
||||||
|
attribute: Annotated[Optional[Literal["abc", "def"]], Form()]
|
||||||
|
):
|
||||||
|
return {"attribute": attribute}
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
# Invalid value should fail
|
||||||
|
response = client.post("/", data={"attribute": "xyz"})
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
# String "None" is also invalid
|
||||||
|
response = client.post("/", data={"attribute": "None"})
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
def test_optional_literal_form_empty_string():
|
||||||
|
"""Test that empty string is treated as None for optional Form fields"""
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.post("/")
|
||||||
|
async def read_main(
|
||||||
|
attribute: Annotated[Optional[Literal["abc", "def"]], Form()]
|
||||||
|
):
|
||||||
|
return {"attribute": attribute}
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
# Empty string is treated as "not provided" for optional Form fields
|
||||||
|
# This is consistent with FastAPI's behavior for Form data
|
||||||
|
response = client.post("/", data={"attribute": ""})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"attribute": None}
|
||||||
Loading…
Reference in New Issue