From b32e65f5af7d428f814b04ed74e9c501bc83bf16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 26 Dec 2025 22:01:36 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Handle=20Pydantic=20v1=20i?= =?UTF-8?q?mports=20for=20checks=20conditionally,=20filtering=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat/__init__.py | 2 ++ fastapi/_compat/may_v1.py | 3 --- fastapi/_compat/shared.py | 20 +++++++++++++++++--- fastapi/encoders.py | 9 +++++++-- tests/test_jsonable_encoder.py | 8 ++++---- tests/test_pydantic_v1_error.py | 6 +++++- 6 files changed, 35 insertions(+), 13 deletions(-) diff --git a/fastapi/_compat/__init__.py b/fastapi/_compat/__init__.py index e4fc40cd5..9a002e982 100644 --- a/fastapi/_compat/__init__.py +++ b/fastapi/_compat/__init__.py @@ -31,6 +31,8 @@ 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_pydantic_v1_model_class as is_pydantic_v1_model_class +from .shared import is_pydantic_v1_model_instance as is_pydantic_v1_model_instance from .shared import ( is_uploadfile_or_nonable_uploadfile_annotation as is_uploadfile_or_nonable_uploadfile_annotation, ) diff --git a/fastapi/_compat/may_v1.py b/fastapi/_compat/may_v1.py index 08f10ea03..30c99426d 100644 --- a/fastapi/_compat/may_v1.py +++ b/fastapi/_compat/may_v1.py @@ -1,9 +1,6 @@ from collections.abc import Sequence from typing import Any, Union -from pydantic.v1 import BaseModel as BaseModel -from pydantic.v1.fields import ModelField as ModelField - def _regenerate_error_with_loc( *, errors: Sequence[Any], loc_prefix: tuple[Union[str, int], ...] diff --git a/fastapi/_compat/shared.py b/fastapi/_compat/shared.py index 2e0b5fd7b..419b58f7f 100644 --- a/fastapi/_compat/shared.py +++ b/fastapi/_compat/shared.py @@ -1,6 +1,7 @@ import sys import types import typing +import warnings from collections import deque from collections.abc import Mapping, Sequence from dataclasses import is_dataclass @@ -10,7 +11,6 @@ from typing import ( Union, ) -from fastapi._compat import may_v1 from fastapi.types import UnionType from pydantic import BaseModel from pydantic.version import VERSION as PYDANTIC_VERSION @@ -177,13 +177,27 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool: ) +def is_pydantic_v1_model_instance(obj: Any) -> bool: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 + return isinstance(obj, v1.BaseModel) + + +def is_pydantic_v1_model_class(cls: Any) -> bool: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 + return lenient_issubclass(cls, v1.BaseModel) + + def annotation_is_pydantic_v1(annotation: Any) -> bool: - if lenient_issubclass(annotation, may_v1.BaseModel): + if is_pydantic_v1_model_class(annotation): return True origin = get_origin(annotation) if origin is Union or origin is UnionType: for arg in get_args(annotation): - if lenient_issubclass(arg, may_v1.BaseModel): + if is_pydantic_v1_model_class(arg): return True if field_annotation_is_sequence(annotation): for sub_annotation in get_args(annotation): diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 453ed463c..b2be91bd6 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -25,7 +25,12 @@ from pydantic.color import Color from pydantic.networks import AnyUrl, NameEmail from pydantic.types import SecretBytes, SecretStr -from ._compat import Url, _is_undefined, _model_dump, may_v1 +from ._compat import ( + Url, + _is_undefined, + _model_dump, + is_pydantic_v1_model_instance, +) # Taken from Pydantic v1 as is @@ -218,7 +223,7 @@ def jsonable_encoder( include = set(include) if exclude is not None and not isinstance(exclude, (set, dict)): exclude = set(exclude) - if isinstance(obj, may_v1.BaseModel): + if is_pydantic_v1_model_instance(obj): raise PydanticV1NotSupportedError( "pydantic.v1 models are no longer supported by FastAPI." f" Please update the model {obj!r}." diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index e2319221b..4528dff44 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -1,3 +1,4 @@ +import warnings from collections import deque from dataclasses import dataclass from datetime import datetime, timezone @@ -13,8 +14,6 @@ from fastapi.encoders import jsonable_encoder from fastapi.exceptions import PydanticV1NotSupportedError from pydantic import BaseModel, Field, ValidationError -from .utils import needs_pydanticv1 - class Person: def __init__(self, name: str): @@ -157,9 +156,10 @@ def test_encode_custom_json_encoders_model_pydanticv2(): assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} -@needs_pydanticv1 def test_json_encoder_error_with_pydanticv1(): - from pydantic import v1 + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic import v1 class ModelV1(v1.BaseModel): name: str diff --git a/tests/test_pydantic_v1_error.py b/tests/test_pydantic_v1_error.py index 2b574bac7..10a0126ff 100644 --- a/tests/test_pydantic_v1_error.py +++ b/tests/test_pydantic_v1_error.py @@ -1,4 +1,5 @@ import sys +import warnings import pytest @@ -8,9 +9,12 @@ if sys.version_info >= (3, 14): skip_module_if_py_gte_314() from fastapi import FastAPI -from fastapi._compat.may_v1 import BaseModel from fastapi.exceptions import PydanticV1NotSupportedError +with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + from pydantic.v1 import BaseModel + def test_raises_pydantic_v1_model_in_endpoint_param() -> None: class ParamModelV1(BaseModel):