From cda2003ff55b6aa38d8dbb253bbdf281d5c16d33 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Mon, 9 Feb 2026 12:04:36 +0100 Subject: [PATCH] Fix --- fastapi/_compat/shared.py | 8 ++ tests/test_nested_annotated_in_sequence.py | 136 +++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 tests/test_nested_annotated_in_sequence.py diff --git a/fastapi/_compat/shared.py b/fastapi/_compat/shared.py index c009da8fd..db4cb541b 100644 --- a/fastapi/_compat/shared.py +++ b/fastapi/_compat/shared.py @@ -65,6 +65,10 @@ def _annotation_is_sequence(annotation: Union[type[Any], None]) -> bool: def field_annotation_is_sequence(annotation: Union[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): @@ -110,6 +114,10 @@ def field_annotation_is_scalar(annotation: Any) -> bool: def field_annotation_is_scalar_sequence(annotation: Union[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 index 000000000..09e4363e2 --- /dev/null +++ b/tests/test_nested_annotated_in_sequence.py @@ -0,0 +1,136 @@ +from typing import Annotated, Union + +from dirty_equals import IsList +from fastapi import FastAPI, Query +from fastapi.testclient import TestClient +from pydantic import Field + +MaxSizedSet = Annotated[set[str], Field(max_length=3)] + +app = FastAPI() + + +@app.get("/") +def read_root(foo: Annotated[Union[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() == { + "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() == { + "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", + }, + }, + }, + }