From 8b82ec693c9edb702f190bda7356e80faaddcf24 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Tue, 17 Feb 2026 18:09:44 +0000 Subject: [PATCH 1/2] Fix nested pydantic v1 model detection in pydantic integration --- strawberry/experimental/pydantic/_compat.py | 28 ++++++++++++ .../experimental/pydantic/error_type.py | 10 ++--- strawberry/experimental/pydantic/fields.py | 6 +-- strawberry/experimental/pydantic/utils.py | 6 +-- .../pydantic/schema/test_1_and_2.py | 45 +++++++++++++++++++ 5 files changed, 83 insertions(+), 12 deletions(-) diff --git a/strawberry/experimental/pydantic/_compat.py b/strawberry/experimental/pydantic/_compat.py index 84e7273034..74425a2e6b 100644 --- a/strawberry/experimental/pydantic/_compat.py +++ b/strawberry/experimental/pydantic/_compat.py @@ -19,6 +19,23 @@ IS_PYDANTIC_V1: bool = not IS_PYDANTIC_V2 +def _get_base_model_types() -> tuple[type[Any], ...]: + model_types: list[type[Any]] = [BaseModel] + + try: + from pydantic.v1 import BaseModel as BaseModelV1 + except ImportError: + return tuple(model_types) + + if BaseModelV1 not in model_types: + model_types.append(BaseModelV1) + + return tuple(model_types) + + +BASE_MODEL_TYPES = _get_base_model_types() + + @dataclass class CompatModelField: name: str @@ -318,10 +335,21 @@ def new_type_supertype(type_: Any) -> Any: smart_deepcopy, ) + +def is_model_class(type_: Any) -> bool: + return lenient_issubclass(type_, BASE_MODEL_TYPES) + + +def is_model_instance(value: Any) -> bool: + return isinstance(value, BASE_MODEL_TYPES) + + __all__ = [ "PydanticCompat", "get_args", "get_origin", + "is_model_class", + "is_model_instance", "is_new_type", "lenient_issubclass", "new_type_supertype", diff --git a/strawberry/experimental/pydantic/error_type.py b/strawberry/experimental/pydantic/error_type.py index 37ef329425..eade90d670 100644 --- a/strawberry/experimental/pydantic/error_type.py +++ b/strawberry/experimental/pydantic/error_type.py @@ -9,12 +9,10 @@ cast, ) -from pydantic import BaseModel - from strawberry.experimental.pydantic._compat import ( CompatModelField, PydanticCompat, - lenient_issubclass, + is_model_class, ) from strawberry.experimental.pydantic.utils import ( get_private_fields, @@ -31,6 +29,8 @@ if TYPE_CHECKING: from collections.abc import Callable, Sequence + from pydantic import BaseModel + from strawberry.types.base import WithStrawberryObjectDefinition @@ -49,13 +49,13 @@ def field_type_to_type(type_: type) -> Any | list[Any] | None: if is_list(child_type): strawberry_type = field_type_to_type(child_type) - elif lenient_issubclass(child_type, BaseModel): + elif is_model_class(child_type): strawberry_type = get_strawberry_type_from_model(child_type) else: strawberry_type = list[error_class] strawberry_type = Optional[strawberry_type] # noqa: UP045 - elif lenient_issubclass(type_, BaseModel): + elif is_model_class(type_): strawberry_type = get_strawberry_type_from_model(type_) return Optional[strawberry_type] # noqa: UP045 diff --git a/strawberry/experimental/pydantic/fields.py b/strawberry/experimental/pydantic/fields.py index 6a317fe41f..176ba23058 100644 --- a/strawberry/experimental/pydantic/fields.py +++ b/strawberry/experimental/pydantic/fields.py @@ -7,13 +7,11 @@ ) from typing import GenericAlias as TypingGenericAlias # type: ignore -from pydantic import BaseModel - from strawberry.experimental.pydantic._compat import ( PydanticCompat, get_args, get_origin, - lenient_issubclass, + is_model_class, ) from strawberry.experimental.pydantic.exceptions import ( UnregisteredTypeException, @@ -22,7 +20,7 @@ def replace_pydantic_types(type_: Any, is_input: bool) -> Any: - if lenient_issubclass(type_, BaseModel): + if is_model_class(type_): attr = "_strawberry_input_type" if is_input else "_strawberry_type" if hasattr(type_, attr): return getattr(type_, attr) diff --git a/strawberry/experimental/pydantic/utils.py b/strawberry/experimental/pydantic/utils.py index 226589f53c..91ed58fc59 100644 --- a/strawberry/experimental/pydantic/utils.py +++ b/strawberry/experimental/pydantic/utils.py @@ -8,11 +8,10 @@ cast, ) -from pydantic import BaseModel - from strawberry.experimental.pydantic._compat import ( CompatModelField, PydanticCompat, + is_model_instance, smart_deepcopy, ) from strawberry.experimental.pydantic.exceptions import ( @@ -30,6 +29,7 @@ ) if TYPE_CHECKING: + from pydantic import BaseModel from pydantic.typing import NoArgAnyCallable @@ -102,7 +102,7 @@ def get_default_factory_for_field( # if the default value is a pydantic base model # we should return the serialized version of that default for # printing the value. - if isinstance(default, BaseModel): + if is_model_instance(default): return lambda: compat.model_dump(default) return lambda: smart_deepcopy(default) diff --git a/tests/experimental/pydantic/schema/test_1_and_2.py b/tests/experimental/pydantic/schema/test_1_and_2.py index 2716bdc6ba..4b134548be 100644 --- a/tests/experimental/pydantic/schema/test_1_and_2.py +++ b/tests/experimental/pydantic/schema/test_1_and_2.py @@ -87,3 +87,48 @@ def user(self, id: strawberry.ID) -> User | LegacyUser: assert not result.errors assert result.data == {"user": {"__typename": "LegacyUser", "name": "legacy"}} + + +@pytest.mark.skipif( + sys.version_info >= (3, 14), + reason="Pydantic v1 is not compatible with Python 3.14+", +) +@needs_pydantic_v2 +def test_can_use_nested_pydantic_v1_models(): + from pydantic import v1 as pydantic_v1 + + class Book(pydantic_v1.BaseModel): + title: str + + class Library(pydantic_v1.BaseModel): + books: list[Book] + + @strawberry.experimental.pydantic.type(model=Book, all_fields=True) + class BookType: + pass + + @strawberry.experimental.pydantic.type(model=Library, all_fields=True) + class LibraryType: + pass + + @strawberry.type + class Query: + library: LibraryType + + schema = strawberry.Schema(query=Query) + + expected_schema = """ + type BookType { + title: String! + } + + type LibraryType { + books: [BookType!]! + } + + type Query { + library: LibraryType! + } + """ + + assert str(schema) == textwrap.dedent(expected_schema).strip() From 18ef09aebd56ca1622ceed8db665a2098a3a25d4 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Tue, 17 Feb 2026 18:13:31 +0000 Subject: [PATCH 2/2] Add RELEASE.md for pydantic v1 nested model fix --- RELEASE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..9d46f15a3e --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,5 @@ +Release type: patch + +Fix `strawberry.experimental.pydantic` to correctly handle nested `pydantic.v1` +models when running on Pydantic 2 (for example, `list[LegacyModel]` fields with +`all_fields=True`), and add a regression test for this case.