This commit is contained in:
Reinaldo Loyo 2026-02-06 19:08:43 +00:00 committed by GitHub
commit 7b42b6ed6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 98 additions and 1 deletions

View File

@ -352,6 +352,21 @@ def add_non_field_param_to_dependency(
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
class ParamDetails:
type_annotation: Any
@ -420,7 +435,12 @@ def analyze_param(
assert not is_path_param, "Path parameters cannot have default values"
field_info.default = value
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
elif isinstance(fastapi_annotation, params.Depends):
depends = fastapi_annotation

View File

@ -0,0 +1,77 @@
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}