From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Tue, 2 Dec 2025 07:10:27 +0000 (+0100) Subject: 🐛 Fix optional sequence handling in `serialize sequence value` with Pydantic V2 ... X-Git-Tag: 0.123.3~4 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=0f613d9051ce8a84b19c3786647a6d0cfc7973f6;p=thirdparty%2Ffastapi%2Ffastapi.git 🐛 Fix optional sequence handling in `serialize sequence value` with Pydantic V2 (#14297) Co-authored-by: Sebastián Ramírez --- diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 7196a6190f..3d91814c08 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -371,6 +371,13 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: origin_type = get_origin(field.field_info.annotation) or field.field_info.annotation + if origin_type is Union: # Handle optional sequences + union_args = get_args(field.field_info.annotation) + for union_arg in union_args: + if union_arg is type(None): + continue + origin_type = get_origin(union_arg) or union_arg + break assert issubclass(origin_type, shared.sequence_types) # type: ignore[arg-type] return shared.sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return] diff --git a/tests/test_compat.py b/tests/test_compat.py index 0184c9a2ee..c3a97209a9 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -136,6 +136,30 @@ def test_is_uploadfile_sequence_annotation(): assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]]) +@needs_pydanticv2 +def test_serialize_sequence_value_with_optional_list(): + """Test that serialize_sequence_value handles optional lists correctly.""" + from fastapi._compat import v2 + + field_info = FieldInfo(annotation=Union[List[str], None]) + field = v2.ModelField(name="items", field_info=field_info) + result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"]) + assert result == ["a", "b", "c"] + assert isinstance(result, list) + + +@needs_pydanticv2 +def test_serialize_sequence_value_with_none_first_in_union(): + """Test that serialize_sequence_value handles Union[None, List[...]] correctly.""" + from fastapi._compat import v2 + + field_info = FieldInfo(annotation=Union[None, List[str]]) + field = v2.ModelField(name="items", field_info=field_info) + result = v2.serialize_sequence_value(field=field, value=["x", "y"]) + assert result == ["x", "y"] + assert isinstance(result, list) + + @needs_py_lt_314 def test_is_pv1_scalar_field(): from fastapi._compat import v1 diff --git a/tests/test_optional_file_list.py b/tests/test_optional_file_list.py new file mode 100644 index 0000000000..0228900cf6 --- /dev/null +++ b/tests/test_optional_file_list.py @@ -0,0 +1,30 @@ +from typing import List, Optional + +from fastapi import FastAPI, File +from fastapi.testclient import TestClient + +app = FastAPI() + + +@app.post("/files") +async def upload_files(files: Optional[List[bytes]] = File(None)): + if files is None: + return {"files_count": 0} + return {"files_count": len(files), "sizes": [len(f) for f in files]} + + +def test_optional_bytes_list(): + client = TestClient(app) + response = client.post( + "/files", + files=[("files", b"content1"), ("files", b"content2")], + ) + assert response.status_code == 200 + assert response.json() == {"files_count": 2, "sizes": [8, 8]} + + +def test_optional_bytes_list_no_files(): + client = TestClient(app) + response = client.post("/files") + assert response.status_code == 200 + assert response.json() == {"files_count": 0}