Browse Source

fix(compat): route v1/v2 ModelField correctly; expose schema error; harden v2 fallback

- add shared.is_v1_field_info() helper and use it in create_model_field
- build v1_kwargs / v2_kwargs separately; avoid constructing v2 FieldInfo in v1 path
- v2.ModelField: fallback to Any when field_info.annotation is absent (v1 FieldInfo)
- expose PydanticSchemaGenerationError in v1 bridge (map to ConfigError on v1)
- keep stubs minimal (.pyi) and py38-compatible (typing.*)
pull/14201/head
Roberto Bertó 9 months ago
parent
commit
7b8992ab4b
  1. 2
      fastapi/_compat/__init__.py
  2. 25
      fastapi/_compat/shared.py
  3. 24
      fastapi/_compat/v1.py
  4. 19
      fastapi/_compat/v2.py
  5. 41
      fastapi/utils.py

2
fastapi/_compat/__init__.py

@ -85,6 +85,7 @@ 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 is_v1_field_info as is_v1_field_info
from .shared import (
is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation,
)
@ -155,6 +156,7 @@ __all__ = [
"_is_undefined",
"get_cached_model_fields",
"get_compat_model_name_map",
"is_v1_field_info",
"ModelField",
"PYDANTIC_VERSION_MINOR_TUPLE",
"annotation_is_pydantic_v1",

25
fastapi/_compat/shared.py

@ -18,6 +18,11 @@ from typing import (
from fastapi._compat import v1 as _v1
from fastapi.types import UnionType
try:
from fastapi._compat import _v1_params
except Exception: # pragma: no cover
_v1_params = None # type: ignore[assignment]
from pydantic import BaseModel
from pydantic.version import VERSION as PYDANTIC_VERSION
from starlette.datastructures import UploadFile
@ -124,6 +129,26 @@ def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
)
def is_v1_field_info(field_info: Any) -> bool:
"""Return True if this FieldInfo comes from Pydantic v1 (native or shim)."""
if field_info is None:
return False
# v1 native FieldInfo
if isinstance(field_info, getattr(_v1, "FieldInfo", ())):
return True
# shim _v1_params classes
if _v1_params is not None:
for name in ("Param", "Body", "Form", "File", "Query", "Path", "Header", "Cookie"):
cls = getattr(_v1_params, name, None)
if cls and isinstance(field_info, cls):
return True
# wrapper that stores v1.FieldInfo internally
return getattr(field_info, "_fi", None) is not None
def field_annotation_is_scalar(annotation: Any) -> bool:
# handle Ellipsis here to make tuple[int, ...] work nicely
return annotation is Ellipsis or not field_annotation_is_complex(annotation)

24
fastapi/_compat/v1.py

@ -22,6 +22,22 @@ from typing_extensions import Literal
# Legacy FastAPI sentinel used by v1 params
RequiredParam = Ellipsis
# Compat: expose PydanticSchemaGenerationError for both v1 and v2
try:
# pydantic v2 (caso alguém injete v2 por engano)
from pydantic.errors import (
PydanticSchemaGenerationError as _PSGE, # type: ignore[attr-defined]
)
except Exception:
try:
# pydantic v1: usar o erro mais próximo semanticamente
from pydantic.errors import ConfigError as _PSGE
except Exception:
class _PSGE(Exception): # fallback defensivo (não deve acontecer)
pass
PydanticSchemaGenerationError = _PSGE
_pv1 = None
_warned = False
@ -226,3 +242,11 @@ GetJsonSchemaHandler = Any
JsonSchemaValue = Dict[str, Any]
CoreSchema = Any
Url = Any
__all__ = [
"PydanticSchemaGenerationError",
"RequiredParam",
"JsonSchemaValue",
"CoreSchema",
"Url",
]

19
fastapi/_compat/v2.py

@ -106,19 +106,12 @@ class ModelField:
# NOTE: Pydantic v1.FieldInfo does not define .annotation (added in v2).
# When bridging v1->v2, fall back to Any to avoid AttributeError at runtime.
annotation = getattr(self.field_info, "annotation", None)
if annotation is not None:
annotated_type = Annotated[annotation, self.field_info]
else:
# Fallback for v1.FieldInfo (no .annotation)
# Use a simple type that TypeAdapter can handle
annotated_type = Annotated[str, self.field_info]
try:
self._type_adapter: TypeAdapter[Any] = TypeAdapter(annotated_type)
except (AttributeError, TypeError, Exception):
# If TypeAdapter fails for any reason, use a simple fallback
# This handles cases where the annotation type is not compatible with TypeAdapter
self._type_adapter: TypeAdapter[Any] = TypeAdapter(str)
if annotation is None:
annotation = Any
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
Annotated[annotation, self.field_info]
)
def get_default(self) -> Any:
if self.field_info.is_required():

41
fastapi/utils.py

@ -23,6 +23,7 @@ from fastapi._compat import (
Undefined,
UndefinedType,
Validator,
annotation_is_pydantic_v1,
lenient_issubclass,
)
from fastapi.datastructures import DefaultPlaceholder, DefaultType
@ -94,32 +95,44 @@ def create_model_field(
) -> ModelField:
class_validators = class_validators or {}
v1_model_config = _get_v1().BaseConfig
v1_field_info = field_info or FieldInfo()
v1_kwargs = {
# Build v1 kwargs
v1_kwargs: Dict[str, Any] = {
"name": name,
"field_info": v1_field_info,
"type_": type_,
"class_validators": class_validators,
"default": default,
"required": required,
"model_config": v1_model_config,
"model_config": model_config,
"field_info": field_info,
"alias": alias,
}
if PYDANTIC_V2 and version != "1":
from ._compat import v2
# Detect v1 context
from fastapi._compat.shared import is_v1_field_info
field_info = field_info or FieldInfo(
annotation=type_, default=default, alias=alias
)
kwargs = {"mode": mode, "name": name, "field_info": field_info}
is_v1_model = annotation_is_pydantic_v1(type_) or lenient_issubclass(type_, _get_v1().BaseModel)
is_v1_field = is_v1_field_info(field_info)
is_forced_v1 = (version == "1")
# Route to appropriate ModelField
if is_v1_model or is_v1_field or is_forced_v1:
try:
return _get_v1().ModelField(**v1_kwargs) # type: ignore[no-any-return]
except RuntimeError:
raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None
# v2 path - create FieldInfo only for v2
fi_v2 = field_info or FieldInfo(annotation=type_, default=default, alias=alias)
v2_kwargs: Dict[str, Any] = {"mode": mode, "name": name, "field_info": fi_v2}
if PYDANTIC_V2:
from ._compat import v2
try:
return v2.ModelField(**kwargs) # type: ignore[return-value]
return v2.ModelField(**v2_kwargs) # type: ignore[return-value]
except PydanticSchemaGenerationError:
raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None
# Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be
# a Pydantic v1 type, like a constrained int
# Fallback to v1 if v2 not available
try:
return _get_v1().ModelField(**v1_kwargs) # type: ignore[no-any-return]
except RuntimeError:

Loading…
Cancel
Save