]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
🐛 Fix handling sequences with nested Annotated types (#14874)
authorYurii Motov <109919500+YuriiMotov@users.noreply.github.com>
Tue, 28 Jul 2026 14:04:16 +0000 (16:04 +0200)
committerGitHub <noreply@github.com>
Tue, 28 Jul 2026 14:04:16 +0000 (16:04 +0200)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
fastapi/_compat/shared.py
tests/test_nested_annotated_in_sequence.py [new file with mode: 0644]

index bd38c553442dfb145dd8c73145c242ec3311aedc..8d4720a2ed3a31531efaf3e0bc3cf4b2a3763ac5 100644 (file)
@@ -63,6 +63,10 @@ def _annotation_is_sequence(annotation: type[Any] | None) -> bool:
 
 def field_annotation_is_sequence(annotation: type[Any] | None) -> bool:
     origin = get_origin(annotation)
+
+    if origin is Annotated:
+        return field_annotation_is_sequence(get_args(annotation)[0])
+
     if origin is Union or origin is UnionType:
         for arg in get_args(annotation):
             if field_annotation_is_sequence(arg):
@@ -108,6 +112,10 @@ def field_annotation_is_scalar(annotation: Any) -> bool:
 
 def field_annotation_is_scalar_sequence(annotation: type[Any] | None) -> bool:
     origin = get_origin(annotation)
+
+    if origin is Annotated:
+        return field_annotation_is_scalar_sequence(get_args(annotation)[0])
+
     if origin is Union or origin is UnionType:
         at_least_one_scalar_sequence = False
         for arg in get_args(annotation):
diff --git a/tests/test_nested_annotated_in_sequence.py b/tests/test_nested_annotated_in_sequence.py
new file mode 100644 (file)
index 0000000..4a725c2
--- /dev/null
@@ -0,0 +1,143 @@
+from typing import Annotated
+
+from dirty_equals import IsList
+from fastapi import FastAPI, Query
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+from pydantic import Field
+
+MaxSizedSet = Annotated[set[str], Field(max_length=3)]
+
+app = FastAPI()
+
+
+@app.get("/")
+def read_root(foo: Annotated[MaxSizedSet | None, Query()] = None):
+    return {"foo": foo}
+
+
+client = TestClient(app)
+
+
+def test_endpoint_none():
+    response = client.get("/")
+    assert response.status_code == 200
+    assert response.json() == {"foo": None}
+
+
+def test_endpoint_valid():
+    response = client.get("/", params={"foo": ["a", "b"]})
+    assert response.status_code == 200
+    assert response.json() == {"foo": IsList("a", "b", check_order=False)}
+
+
+def test_endpoint_too_long():
+    response = client.get("/", params={"foo": ["a", "b", "c", "d"]})
+    assert response.status_code == 422
+    assert response.json() == snapshot(
+        {
+            "detail": [
+                {
+                    "type": "too_long",
+                    "loc": ["query", "foo"],
+                    "msg": "Set should have at most 3 items after validation, not more",
+                    "input": IsList("a", "b", "c", "d", check_order=False),
+                    "ctx": {
+                        "actual_length": None,
+                        "field_type": "Set",
+                        "max_length": 3,
+                    },
+                }
+            ]
+        }
+    )
+
+
+def test_openapi():
+    assert app.openapi() == snapshot(
+        {
+            "components": {
+                "schemas": {
+                    "HTTPValidationError": {
+                        "properties": {
+                            "detail": {
+                                "items": {
+                                    "$ref": "#/components/schemas/ValidationError"
+                                },
+                                "title": "Detail",
+                                "type": "array",
+                            },
+                        },
+                        "title": "HTTPValidationError",
+                        "type": "object",
+                    },
+                    "ValidationError": {
+                        "properties": {
+                            "ctx": {"title": "Context", "type": "object"},
+                            "input": {"title": "Input"},
+                            "loc": {
+                                "items": {
+                                    "anyOf": [{"type": "string"}, {"type": "integer"}],
+                                },
+                                "title": "Location",
+                                "type": "array",
+                            },
+                            "msg": {"title": "Message", "type": "string"},
+                            "type": {"title": "Error Type", "type": "string"},
+                        },
+                        "required": ["loc", "msg", "type"],
+                        "title": "ValidationError",
+                        "type": "object",
+                    },
+                },
+            },
+            "info": {
+                "title": "FastAPI",
+                "version": "0.1.0",
+            },
+            "openapi": "3.1.0",
+            "paths": {
+                "/": {
+                    "get": {
+                        "operationId": "read_root__get",
+                        "parameters": [
+                            {
+                                "in": "query",
+                                "name": "foo",
+                                "required": False,
+                                "schema": {
+                                    "anyOf": [
+                                        {
+                                            "items": {"type": "string"},
+                                            "maxItems": 3,
+                                            "type": "array",
+                                            "uniqueItems": True,
+                                        },
+                                        {"type": "null"},
+                                    ],
+                                    "title": "Foo",
+                                },
+                            },
+                        ],
+                        "responses": {
+                            "200": {
+                                "content": {"application/json": {"schema": {}}},
+                                "description": "Successful Response",
+                            },
+                            "422": {
+                                "content": {
+                                    "application/json": {
+                                        "schema": {
+                                            "$ref": "#/components/schemas/HTTPValidationError",
+                                        },
+                                    },
+                                },
+                                "description": "Validation Error",
+                            },
+                        },
+                        "summary": "Read Root",
+                    },
+                },
+            },
+        }
+    )