|
|
@ -15,6 +15,7 @@ from collections.abc import ( |
|
|
from contextlib import AsyncExitStack, contextmanager |
|
|
from contextlib import AsyncExitStack, contextmanager |
|
|
from copy import copy, deepcopy |
|
|
from copy import copy, deepcopy |
|
|
from dataclasses import dataclass |
|
|
from dataclasses import dataclass |
|
|
|
|
|
from functools import lru_cache |
|
|
from typing import ( |
|
|
from typing import ( |
|
|
Annotated, |
|
|
Annotated, |
|
|
Any, |
|
|
Any, |
|
|
@ -64,11 +65,10 @@ from fastapi.utils import ( |
|
|
create_model_field, |
|
|
create_model_field, |
|
|
get_path_param_names, |
|
|
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 pydantic.fields import FieldInfo |
|
|
from starlette.background import BackgroundTasks as StarletteBackgroundTasks |
|
|
from starlette.background import BackgroundTasks as StarletteBackgroundTasks |
|
|
from starlette.concurrency import run_in_threadpool |
|
|
from starlette.concurrency import run_in_threadpool |
|
|
from functools import lru_cache |
|
|
|
|
|
from starlette.datastructures import ( |
|
|
from starlette.datastructures import ( |
|
|
FormData, |
|
|
FormData, |
|
|
Headers, |
|
|
Headers, |
|
|
@ -757,12 +757,15 @@ def _validate_value_with_model_field( |
|
|
if isinstance(value, FastAPIOptimizedJsonBytes): |
|
|
if isinstance(value, FastAPIOptimizedJsonBytes): |
|
|
return field.validate_json(value, values, loc=loc) |
|
|
return field.validate_json(value, values, loc=loc) |
|
|
return field.validate(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 |
|
|
# 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"') |
|
|
# 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: |
|
|
try: |
|
|
import json |
|
|
import json |
|
|
|
|
|
|
|
|
value = json.loads(value) |
|
|
value = json.loads(value) |
|
|
except json.JSONDecodeError: |
|
|
except json.JSONDecodeError: |
|
|
pass |
|
|
pass |
|
|
@ -888,10 +891,10 @@ def request_params_to_args( |
|
|
|
|
|
|
|
|
if grouped_adapter is not None: |
|
|
if grouped_adapter is not None: |
|
|
try: |
|
|
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. |
|
|
# No, params_to_process is a dict, but from_attributes doesn't hurt. |
|
|
validated_data = grouped_adapter.validate_python(params_to_process) |
|
|
validated_data = grouped_adapter.validate_python(params_to_process) |
|
|
|
|
|
|
|
|
# validated_data is a dict (or BaseModel? TypeAdapter(BaseModel) returns BaseModel) |
|
|
# validated_data is a dict (or BaseModel? TypeAdapter(BaseModel) returns BaseModel) |
|
|
# We need to extract to values dict. |
|
|
# We need to extract to values dict. |
|
|
if hasattr(validated_data, "model_dump"): |
|
|
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) |
|
|
# model_dump might convert inner models, which we don't want (FastAPI keeps them as objects) |
|
|
values.update(validated_data.__dict__) |
|
|
values.update(validated_data.__dict__) |
|
|
else: |
|
|
else: |
|
|
values.update(validated_data) # type: ignore |
|
|
values.update(validated_data) # type: ignore |
|
|
except ValidationError as exc: |
|
|
except ValidationError as exc: |
|
|
field_in = fields[0].field_info.in_.value |
|
|
field_in = fields[0].field_info.in_.value |
|
|
|
|
|
|
|
|
# Map f.name to f.alias in case Pydantic returned the internal name |
|
|
# 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} |
|
|
name_to_alias = {f.name: get_validation_alias(f) for f in fields} |
|
|
|
|
|
|
|
|
for err in exc.errors(include_url=False): |
|
|
for err in exc.errors(include_url=False): |
|
|
err_loc = list(err["loc"]) |
|
|
err_loc = list(err["loc"]) |
|
|
if err_loc and err_loc[0] in name_to_alias: |
|
|
if err_loc and err_loc[0] in name_to_alias: |
|
|
err_loc[0] = name_to_alias[err_loc[0]] # type: ignore |
|
|
err_loc[0] = name_to_alias[err_loc[0]] # type: ignore |
|
|
err["loc"] = (field_in, *err_loc) # type: ignore |
|
|
err["loc"] = (field_in, *err_loc) # type: ignore |
|
|
|
|
|
|
|
|
if err["type"] == "missing": |
|
|
if err["type"] == "missing": |
|
|
err["input"] = None |
|
|
err["input"] = None |
|
|
|
|
|
|
|
|
errors.append(err) # type: ignore |
|
|
errors.append(err) # type: ignore |
|
|
return values, errors |
|
|
return values, errors |
|
|
|
|
|
|
|
|
@ -1051,6 +1054,7 @@ async def request_body_to_args( |
|
|
if isinstance(received_body, bytes): |
|
|
if isinstance(received_body, bytes): |
|
|
try: |
|
|
try: |
|
|
import json |
|
|
import json |
|
|
|
|
|
|
|
|
body_to_process = json.loads(received_body) |
|
|
body_to_process = json.loads(received_body) |
|
|
except json.JSONDecodeError as e: |
|
|
except json.JSONDecodeError as e: |
|
|
return values, [ |
|
|
return values, [ |
|
|
|