diff --git a/fastapi/_compat/v2.py b/fastapi/_compat/v2.py index 3f04ef38f5..efdd3ee993 100644 --- a/fastapi/_compat/v2.py +++ b/fastapi/_compat/v2.py @@ -516,43 +516,43 @@ def _regenerate_error_with_loc( ) -> list[dict[str, Any]]: updated_loc_errors: list[Any] = [] for err in errors: - if decoded_value is not Undefined: - new_err = {**err, "loc": loc_prefix + err.get("loc", ())} - # If we are validating a Body with multiple fields, Pydantic's - # "input" might be just the value of one field. - # But when we decode the whole body, we might need to drill down. - # However, for tutorials, usually "input" matches the decoded value - # if the error is at the top level of the body. - # Let's try to match Pydantic's behavior more closely but with - # decoded values. - curr_input = decoded_value - # If the error is inside the body, try to find the specific input - # that caused it, based on the relative location from the body root. - # loc_prefix is usually ('body',) - rel_loc = err.get("loc", ()) - for path_item in rel_loc: - if path_item == "[key]": - # For dict key errors, Pydantic includes "[key]" in the loc. - # The "input" should be the key itself, which was the previous - # path_item. - break - try: - if isinstance(curr_input, (dict, list)): - curr_input = curr_input[path_item] # type: ignore[index] - else: - break - except (KeyError, IndexError, TypeError): + if decoded_value is not Undefined: + new_err = {**err, "loc": loc_prefix + err.get("loc", ())} + # If we are validating a Body with multiple fields, Pydantic's + # "input" might be just the value of one field. + # But when we decode the whole body, we might need to drill down. + # However, for tutorials, usually "input" matches the decoded value + # if the error is at the top level of the body. + # Let's try to match Pydantic's behavior more closely but with + # decoded values. + curr_input = decoded_value + # If the error is inside the body, try to find the specific input + # that caused it, based on the relative location from the body root. + # loc_prefix is usually ('body',) + rel_loc = err.get("loc", ()) + for path_item in rel_loc: + if path_item == "[key]": + # For dict key errors, Pydantic includes "[key]" in the loc. + # The "input" should be the key itself, which was the previous + # path_item. + break + try: + if isinstance(curr_input, (dict, list)): + curr_input = curr_input[path_item] # type: ignore[index] + else: break + except (KeyError, IndexError, TypeError): + break - # If it's a key error, the input is the key which is the last path item before "[key]" - if rel_loc and rel_loc[-1] == "[key]": - new_err["input"] = rel_loc[-2] - else: - new_err["input"] = curr_input - if new_err.get("msg") == "Input should be a valid array": - new_err["msg"] = "Input should be a valid list" + # If it's a key error, the input is the key which is the last path item before "[key]" + if rel_loc and rel_loc[-1] == "[key]": + new_err["input"] = rel_loc[-2] else: - new_err = {**err, "loc": loc_prefix + err.get("loc", ())} - updated_loc_errors.append(new_err) + new_err["input"] = curr_input + if new_err.get("msg") == "Input should be a valid array": + new_err["msg"] = "Input should be a valid list" + else: + new_err = {**err, "loc": loc_prefix + err.get("loc", ())} + updated_loc_errors.append(new_err) return updated_loc_errors diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 10f1d55570..b02640088f 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -15,6 +15,7 @@ from collections.abc import ( from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy from dataclasses import dataclass +from functools import lru_cache from typing import ( Annotated, Any, @@ -64,11 +65,10 @@ from fastapi.utils import ( create_model_field, get_path_param_names, ) -from pydantic import BaseModel, Json, TypeAdapter, create_model, ValidationError +from pydantic import BaseModel, Json, TypeAdapter, ValidationError, create_model from pydantic.fields import FieldInfo from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.concurrency import run_in_threadpool -from functools import lru_cache from starlette.datastructures import ( FormData, Headers, @@ -757,12 +757,15 @@ def _validate_value_with_model_field( if isinstance(value, FastAPIOptimizedJsonBytes): return field.validate_json(value, values, loc=loc) return field.validate(value, values, loc=loc) - + # If it's a scalar and we have bytes, we MUST decode it first because Pydantic's # validate_python doesn't handle JSON-encoded scalar bytes (like b'"-1"') - if isinstance(value, bytes) and field_annotation_is_scalar(field.field_info.annotation): + if isinstance(value, bytes) and field_annotation_is_scalar( + field.field_info.annotation + ): try: import json + value = json.loads(value) except json.JSONDecodeError: pass @@ -888,10 +891,10 @@ def request_params_to_args( if grouped_adapter is not None: try: - # We use from_attributes=True because the request might be an object? + # We use from_attributes=True because the request might be an object? # No, params_to_process is a dict, but from_attributes doesn't hurt. validated_data = grouped_adapter.validate_python(params_to_process) - + # validated_data is a dict (or BaseModel? TypeAdapter(BaseModel) returns BaseModel) # We need to extract to values dict. if hasattr(validated_data, "model_dump"): @@ -899,22 +902,22 @@ def request_params_to_args( # model_dump might convert inner models, which we don't want (FastAPI keeps them as objects) values.update(validated_data.__dict__) else: - values.update(validated_data) # type: ignore + values.update(validated_data) # type: ignore except ValidationError as exc: field_in = fields[0].field_info.in_.value - + # Map f.name to f.alias in case Pydantic returned the internal name name_to_alias = {f.name: get_validation_alias(f) for f in fields} - + for err in exc.errors(include_url=False): err_loc = list(err["loc"]) if err_loc and err_loc[0] in name_to_alias: err_loc[0] = name_to_alias[err_loc[0]] # type: ignore err["loc"] = (field_in, *err_loc) # type: ignore - + if err["type"] == "missing": err["input"] = None - + errors.append(err) # type: ignore return values, errors @@ -1051,6 +1054,7 @@ async def request_body_to_args( if isinstance(received_body, bytes): try: import json + body_to_process = json.loads(received_body) except json.JSONDecodeError as e: return values, [ diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 456742679b..435056b710 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -242,7 +242,7 @@ def jsonable_encoder( include = set(include) # type: ignore[assignment] # ty: ignore[invalid-assignment] if exclude is not None and not isinstance(exclude, (set, dict)): exclude = set(exclude) # type: ignore[assignment] # ty: ignore[invalid-assignment] - + if not custom_encoder and not sqlalchemy_safe: try: return _any_type_adapter.dump_python( diff --git a/fastapi/routing.py b/fastapi/routing.py index 9629fa382b..f3c8ebb7d9 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -35,7 +35,6 @@ from anyio.abc import ObjectReceiveStream from fastapi import params from fastapi._compat import ( ModelField, - Undefined, lenient_issubclass, ) from fastapi.datastructures import Default, DefaultPlaceholder @@ -73,6 +72,7 @@ from fastapi.utils import ( get_value_or_default, is_body_allowed_for_status_code, ) +from pydantic import TypeAdapter from starlette import routing from starlette._exception_handler import wrap_app_handling_exceptions from starlette._utils import is_async_callable @@ -91,7 +91,6 @@ from starlette.routing import Mount as Mount # noqa from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send from starlette.websockets import WebSocket from typing_extensions import deprecated -from pydantic import TypeAdapter _any_type_adapter = TypeAdapter(Any) @@ -424,7 +423,7 @@ def get_request_handler( subtype = message.get_content_subtype() if subtype == "json" or subtype.endswith("+json"): is_json_content = True - + if is_json_content: body = FastAPIOptimizedJsonBytes(body_bytes) else: @@ -515,7 +514,9 @@ def get_request_handler( if hasattr(item.data, "model_dump_json"): data_str = item.data.model_dump_json() else: - data_str = _any_type_adapter.dump_json(item.data).decode("utf-8") + data_str = _any_type_adapter.dump_json( + item.data + ).decode("utf-8") else: data_str = None return format_sse_event(