Skip to content

Fix nested pydantic.v1 model detection in pydantic integration#4246

Merged
patrick91 merged 2 commits intomainfrom
codex/fix-3584-pydantic-v1-nested-models
Feb 17, 2026
Merged

Fix nested pydantic.v1 model detection in pydantic integration#4246
patrick91 merged 2 commits intomainfrom
codex/fix-3584-pydantic-v1-nested-models

Conversation

@patrick91
Copy link
Member

@patrick91 patrick91 commented Feb 17, 2026

Summary

  • fix pydantic model detection to recognize both pydantic.BaseModel and pydantic.v1.BaseModel under Pydantic 2
  • use that shared detection helper in recursive type replacement for experimental pydantic types
  • align related default/error-type model checks with the same helper
  • add a regression test for nested pydantic.v1 models with all_fields=True

Reproduction

Before this change, the following fails with:
TypeError: LibraryType fields cannot be resolved. Unexpected type '<class ...Book>'

import strawberry
from pydantic.v1 import BaseModel

class Book(BaseModel):
    title: str

class Library(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)

After this change, schema creation succeeds and the generated GraphQL type for books is [BookType!]!.

Tests

  • uv run --python 3.12 pytest tests/experimental/pydantic/schema/test_1_and_2.py -q
  • uv run --python 3.12 pytest tests/experimental/pydantic/test_error_type.py tests/experimental/pydantic/schema/test_defaults.py tests/experimental/pydantic/test_basic.py -q
  • uv run --python 3.12 ruff check strawberry/experimental/pydantic/_compat.py strawberry/experimental/pydantic/fields.py strawberry/experimental/pydantic/error_type.py strawberry/experimental/pydantic/utils.py tests/experimental/pydantic/schema/test_1_and_2.py

Closes #3584

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 17, 2026

Reviewer's Guide

Updates the Strawberry pydantic integration to detect both top-level and nested pydantic.v1 BaseModel types when running under Pydantic v2, centralizing model detection helpers and using them in type replacement, default handling, and error-type generation, plus adds a regression test for nested v1 models.

File-Level Changes

Change Details Files
Add shared helpers to detect pydantic model classes and instances that work with both BaseModel and pydantic.v1.BaseModel when available.
  • Introduce _get_base_model_types to build a tuple of supported BaseModel types, including pydantic.v1.BaseModel when importable
  • Define BASE_MODEL_TYPES constant for reuse throughout the integration
  • Add is_model_class and is_model_instance helpers backed by lenient_issubclass and isinstance against BASE_MODEL_TYPES
  • Export the new helpers from the _compat module via all
strawberry/experimental/pydantic/_compat.py
Use the shared model detection helpers in field/error handling and type replacement logic so nested pydantic.v1 models are correctly recognized.
  • Switch error_type.field_type_to_type from lenient_issubclass(..., BaseModel) to is_model_class for both list element and direct field cases
  • Update utils.get_default_factory_for_field to use is_model_instance instead of isinstance(..., BaseModel) for default model values
  • Update replace_pydantic_types to use is_model_class instead of lenient_issubclass(..., BaseModel) when mapping models to Strawberry types
strawberry/experimental/pydantic/error_type.py
strawberry/experimental/pydantic/utils.py
strawberry/experimental/pydantic/fields.py
Add a regression test ensuring nested pydantic.v1 models work under Pydantic v2 and generate the expected GraphQL schema.
  • Add test_can_use_nested_pydantic_v1_models guarded by Python version and needs_pydantic_v2 marker
  • Define nested pydantic.v1 BaseModel classes (Book and Library) and corresponding Strawberry pydantic types
  • Build a schema with a Query returning LibraryType and assert the printed schema matches the expected SDL
tests/experimental/pydantic/schema/test_1_and_2.py

Assessment against linked issues

Issue Objective Addressed Explanation
#3584 Ensure Strawberry’s pydantic integration correctly recognizes and handles pydantic.v1 BaseModel subclasses (including nested models) under Pydantic v2 so that the example schema with Library(books: list[Book]) builds without TypeError.
#3584 Add a regression test that covers nested pydantic.v1 models to prevent this issue from reoccurring.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@botberry
Copy link
Member

botberry commented Feb 17, 2026

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


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.

Here's the tweet text:

🆕 Release (next) is out! Thanks to @patrick91 for the PR 👏

Get it here 👉 https://strawberry.rocks/release/(next)

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 17, 2026

Greptile Summary

This PR fixes a bug where nested pydantic v1 models (via pydantic.v1 under pydantic v2) were not being recognized during schema type generation and related operations, causing schema build failures for users mixing pydantic v1 and v2 models.

Changes:

  • _compat.py: Introduces _get_base_model_types(), which builds a tuple of base model classes including both pydantic.BaseModel and pydantic.v1.BaseModel (when available). Two new helper functions — is_model_class() and is_model_instance() — use this tuple with lenient_issubclass and isinstance respectively, replacing the previously pydantic-v2-only checks.
  • fields.py: replace_pydantic_types() now uses is_model_class() so that pydantic v1 models under pydantic v2 are correctly resolved to their registered strawberry types during recursive type replacement.
  • error_type.py: field_type_to_type() now uses is_model_class(), fixing model detection for nested fields and list-of-model fields in error types.
  • utils.py: get_default_factory_for_field() now uses is_model_instance(), ensuring pydantic v1 model instances used as field defaults are also serialized via model_dump.
  • tests/: Adds a regression test verifying the schema SDL for nested pydantic v1 models using list[Book] inside Library.

Architecture fit: The fix is well-scoped, centralizing model detection in _compat.py rather than duplicating the version-check logic across multiple files. The approach is consistent with the existing PydanticCompat pattern in the same module.

Minor observation: The new regression test only validates schema SDL generation and does not exercise the actual query execution / from_pydantic conversion path for nested models, which is the most common runtime usage pattern.

Confidence Score: 5/5

  • This PR is safe to merge; the changes are tightly scoped, well-structured, and solve a genuine regression for mixed pydantic v1/v2 setups.
  • The fix is minimal and targeted — it adds a shared detection helper in _compat.py and replaces three isolated BaseModel-only checks across fields.py, error_type.py, and utils.py. The logic correctly handles both pydantic v1-only environments (where pydantic.v1 is not importable) and pydantic v2 environments with the v1 backport. No existing interfaces are changed. The only gap is that the new regression test validates schema SDL only and not query execution, but existing tests cover that path for the pydantic v2 case.
  • No files require special attention; _compat.py is the most important file to review for correctness of the new BASE_MODEL_TYPES construction.

Important Files Changed

Filename Overview
strawberry/experimental/pydantic/_compat.py Adds _get_base_model_types(), BASE_MODEL_TYPES, is_model_class(), and is_model_instance() helpers that correctly recognize both pydantic v2's BaseModel and pydantic v1's BaseModel (via pydantic.v1). is_model_class uses lenient_issubclass, which is defined later in the same module (after the pydantic version conditional block), so the ordering is safe. Minor concern: lenient_issubclass accepts a tuple of types natively (mirrors issubclass), so passing BASE_MODEL_TYPES is correct.
strawberry/experimental/pydantic/error_type.py Replaces direct lenient_issubclass(type_, BaseModel) calls with the new is_model_class(type_) helper, and moves the runtime from pydantic import BaseModel import into a TYPE_CHECKING block. No functional regressions; the new helper correctly handles both pydantic v1 and v2 models.
strawberry/experimental/pydantic/fields.py Replaces lenient_issubclass(type_, BaseModel) with is_model_class(type_) in replace_pydantic_types, correctly extending nested model detection in replace_types_recursively to cover pydantic v1 models under pydantic v2. The runtime BaseModel import is removed as it's no longer needed.
strawberry/experimental/pydantic/utils.py Replaces isinstance(default, BaseModel) with is_model_instance(default) in get_default_factory_for_field, and moves BaseModel import to TYPE_CHECKING. The fix ensures pydantic v1 model instances used as default values are also correctly serialized via model_dump.
tests/experimental/pydantic/schema/test_1_and_2.py Adds a regression test test_can_use_nested_pydantic_v1_models that validates the schema string for nested pydantic v1 models. The test only validates the schema SDL, but omits an execution test (e.g., a query that resolves library data via from_pydantic). This is a minor gap compared to the existing test_can_use_both_pydantic_1_and_2 test which includes execution.

Flowchart

flowchart TD
    A["Model type/instance check needed"] --> B{"is_model_class / is_model_instance"}
    B --> C["BASE_MODEL_TYPES = _get_base_model_types()"]
    C --> D["pydantic.BaseModel (always included)"]
    C --> E{"pydantic.v1 available?"}
    E -- "Yes (Pydantic v2 installed)" --> F["pydantic.v1.BaseModel added"]
    E -- "No (Pydantic v1 only)" --> G["Only pydantic.BaseModel"]
    F --> H["lenient_issubclass(type_, BASE_MODEL_TYPES)"]
    G --> H
    D --> H
    H --> I["fields.py: replace_pydantic_types"]
    H --> J["error_type.py: field_type_to_type"]
    H --> K["utils.py: get_default_factory_for_field"]
Loading

Last reviewed commit: 18ef09a

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +97 to +134
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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test only validates schema SDL, not query execution

The test verifies that the schema SDL is generated correctly for nested pydantic v1 models, which is the core regression being fixed. However, it does not exercise the conversion path (e.g., from_pydantic, resolving a query with a real Library instance containing Book objects). The existing test_can_use_both_pydantic_1_and_2 test includes both schema string validation and query execution.

Consider adding an execution assertion to also cover the replace_types_recursively path at runtime, e.g.:

@strawberry.type
class Query:
    @strawberry.field
    def library(self) -> LibraryType:
        return LibraryType.from_pydantic(Library(books=[Book(title="Test")]))

schema = strawberry.Schema(query=Query)
result = schema.execute_sync("{ library { books { title } } }")
assert not result.errors
assert result.data == {"library": {"books": [{"title": "Test"}]}}

This would validate the full conversion pipeline for nested v1 models, not just schema generation.

Context Used: Context from dashboard - In tests involving Pydantic models, ensure that the tests accurately reflect the intended behavior b... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 17, 2026

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing codex/fix-3584-pydantic-v1-nested-models (18ef09a) with main (7d0dc23)

Open in CodSpeed

@patrick91 patrick91 merged commit 80effb8 into main Feb 17, 2026
28 checks passed
@patrick91 patrick91 deleted the codex/fix-3584-pydantic-v1-nested-models branch February 17, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pydantic TypeError: fields cannot be resolved

3 participants