]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
🐛 Fix optional sequence handling in `serialize sequence value` with Pydantic V2 ...
authorMotov Yurii <109919500+YuriiMotov@users.noreply.github.com>
Tue, 2 Dec 2025 07:10:27 +0000 (08:10 +0100)
committerGitHub <noreply@github.com>
Tue, 2 Dec 2025 07:10:27 +0000 (08:10 +0100)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
fastapi/_compat/v2.py
tests/test_compat.py
tests/test_optional_file_list.py [new file with mode: 0644]

index 7196a6190feae3d40ca2553088a041bec7ad3103..3d91814c08730c0632a314437fd6da2de3253b03 100644 (file)
@@ -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]
 
index 0184c9a2ee8d9c41788f2a70ce74c2aad1542cc6..c3a97209a9cfbdbc1a23861987ad8260c095c21a 100644 (file)
@@ -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 (file)
index 0000000..0228900
--- /dev/null
@@ -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}