diff --git a/docs_src/query_params/tutorial007_py310.py b/docs_src/query_params/tutorial007_py310.py index a88600d5d..ef8b09367 100644 --- a/docs_src/query_params/tutorial007_py310.py +++ b/docs_src/query_params/tutorial007_py310.py @@ -1,6 +1,7 @@ from typing import Annotated, Dict, List, Union from fastapi import FastAPI, Query +from pydantic import OnErrorOmit app = FastAPI() @@ -9,7 +10,7 @@ app = FastAPI() def get_mixed_mapping_mixed_type_query_params( query: Annotated[int, Query()] = None, mapping_query_str_or_int: Annotated[ - Union[Dict[str, str], Dict[str, int]], Query() + Union[Dict[str, OnErrorOmit[str]], Dict[str, int]], Query() ] = None, mapping_query_int: Annotated[Dict[str, int], Query()] = None, sequence_mapping_int: Annotated[Dict[str, List[int]], Query()] = None, diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py index b7287ea92..81327d2a0 100644 --- a/fastapi/_compat/__init__.py +++ b/fastapi/_compat/__init__.py @@ -21,7 +21,6 @@ from .main import get_compat_model_name_map as get_compat_model_name_map from .main import get_definitions as get_definitions from .main import get_missing_field_error as get_missing_field_error from .main import get_schema_from_model_field as get_schema_from_model_field -from .main import ignore_invalid as ignore_invalid from .main import is_bytes_field as is_bytes_field from .main import is_bytes_sequence_field as is_bytes_sequence_field from .main import is_scalar_field as is_scalar_field @@ -29,6 +28,7 @@ from .main import is_scalar_mapping_field as is_scalar_mapping_field from .main import is_scalar_sequence_field as is_scalar_sequence_field from .main import is_scalar_sequence_mapping_field as is_scalar_sequence_mapping_field from .main import is_sequence_field as is_sequence_field +from .main import omit_by_default as omit_by_default from .main import serialize_sequence_value as serialize_sequence_value from .main import ( with_info_plain_validator_function as with_info_plain_validator_function, @@ -42,6 +42,9 @@ from .shared import PYDANTIC_V2 as PYDANTIC_V2 from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE from .shared import annotation_is_pydantic_v1 as annotation_is_pydantic_v1 from .shared import field_annotation_is_scalar as field_annotation_is_scalar +from .shared import ( + field_annotation_is_scalar_sequence_mapping as field_annotation_is_scalar_sequence_mapping, +) from .shared import ( is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation, ) diff --git a/fastapi/_compat/main.py b/fastapi/_compat/main.py index 5a36d887a..e1859018a 100644 --- a/fastapi/_compat/main.py +++ b/fastapi/_compat/main.py @@ -28,6 +28,7 @@ if PYDANTIC_V2: from .v2 import Validator as Validator from .v2 import evaluate_forwardref as evaluate_forwardref from .v2 import get_missing_field_error as get_missing_field_error + from .v2 import omit_by_default as omit_by_default from .v2 import ( with_info_plain_validator_function as with_info_plain_validator_function, ) diff --git a/fastapi/_compat/shared.py b/fastapi/_compat/shared.py index 4540d6ec9..84b72adf4 100644 --- a/fastapi/_compat/shared.py +++ b/fastapi/_compat/shared.py @@ -5,7 +5,6 @@ from collections import deque from dataclasses import is_dataclass from typing import ( Any, - Callable, Deque, FrozenSet, List, @@ -19,7 +18,7 @@ from typing import ( from fastapi._compat import may_v1 from fastapi.types import UnionType -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel from pydantic.version import VERSION as PYDANTIC_VERSION from starlette.datastructures import UploadFile from typing_extensions import Annotated, get_args, get_origin diff --git a/fastapi/_compat/v1.py b/fastapi/_compat/v1.py index 8e623d065..1d5c83fa7 100644 --- a/fastapi/_compat/v1.py +++ b/fastapi/_compat/v1.py @@ -348,4 +348,3 @@ def create_body_model( def get_model_fields(model: Type[BaseModel]) -> List[ModelField]: return list(model.__fields__.values()) # type: ignore[attr-defined] - diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 1908d3df8..5255a5208 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -17,8 +17,8 @@ from typing import ( from fastapi._compat import may_v1, shared from fastapi.openapi.constants import REF_TEMPLATE -from fastapi.types import IncEx, ModelNameMap -from pydantic import BaseModel, TypeAdapter, WrapValidator, create_model +from fastapi.types import IncEx, ModelNameMap, UnionType +from pydantic import BaseModel, OnErrorOmit, TypeAdapter, create_model from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError from pydantic import PydanticUndefinedAnnotation as PydanticUndefinedAnnotation from pydantic import ValidationError as ValidationError @@ -488,3 +488,21 @@ def get_flat_models_from_fields( def get_long_model_name(model: TypeModelOrEnum) -> str: return f"{model.__module__}__{model.__qualname__}".replace(".", "__") + +def omit_by_default(annotation: Any) -> Any: + # Update the annotation to use OnErrorOmit for the inner type(s) + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + new_args = [] + for arg in get_args(annotation): + new_arg = omit_by_default(arg) + new_args.append(new_arg) + return Union[tuple(new_args)] # type: ignore[return-value] + elif origin is Annotated: + annotated_args = get_args(annotation) + base_annotation = annotated_args[0] + new_base_annotation = omit_by_default(base_annotation) + new_metadata = annotated_args[1:] + return Annotated[new_base_annotation + new_metadata] # type: ignore[return-value] + else: + return OnErrorOmit[annotation] # type: ignore[return-value] diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index b39cf3482..a42492ca8 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -31,6 +31,7 @@ from fastapi._compat import ( create_body_model, evaluate_forwardref, field_annotation_is_scalar, + field_annotation_is_scalar_sequence_mapping, get_annotation_from_field_info, get_cached_model_fields, get_missing_field_error, @@ -45,6 +46,7 @@ from fastapi._compat import ( is_uploadfile_sequence_annotation, lenient_issubclass, may_v1, + omit_by_default, sequence_types, serialize_sequence_value, value_is_sequence, @@ -63,7 +65,7 @@ from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.types import DependencyCacheKey from fastapi.utils import create_model_field, get_path_param_names -from pydantic import BaseModel, TypeAdapter +from pydantic import BaseModel from pydantic.fields import FieldInfo from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.concurrency import run_in_threadpool @@ -485,6 +487,12 @@ def analyze_param( and getattr(field_info, "in_", None) is None ): field_info.in_ = params.ParamTypes.query + + if isinstance( + field_info, (params.Query, temp_pydantic_v1_params.Query) + ) and field_annotation_is_scalar_sequence_mapping(use_annotation): + use_annotation = omit_by_default(use_annotation) + use_annotation_from_field_info = get_annotation_from_field_info( use_annotation, field_info, @@ -523,10 +531,6 @@ def analyze_param( and getattr(field, "shape", 1) == 1 ) ) - if is_scalar_sequence_field(field) or is_scalar_sequence_mapping_field( - field - ): - field._type_adapter.core_schema["on_error"] = "omit" return ParamDetails(type_annotation=type_annotation, depends=depends, field=field) diff --git a/fastapi/params.py b/fastapi/params.py index 6a58d5808..3d9319030 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -305,6 +305,7 @@ class Query(Param): # type: ignore[misc] json_schema_extra=json_schema_extra, **extra, ) + self.annotation = self.annotation class Header(Param): # type: ignore[misc] diff --git a/tests/test_tutorial/test_query_params/test_tutorial007_py310.py b/tests/test_tutorial/test_query_params/test_tutorial007_py310.py index dd9a9f850..df9505bfe 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial007_py310.py +++ b/tests/test_tutorial/test_query_params/test_tutorial007_py310.py @@ -17,7 +17,7 @@ def test_foo_needy_very(client: TestClient): "query": 2, "mapping_query_str_or_int": {"foo": "baz"}, "mapping_query_int": {}, - "sequence_mapping_int": {}, + "sequence_mapping_int": {"foo": []}, }