Skip to content

Conversation

Boudewijn26
Copy link

@Boudewijn26 Boudewijn26 commented Sep 23, 2025

Description

This fixes the conversion of pydantic models into strawberry models if those models include generics. It also adds the ability to pass a SchemaDirective to the pydantic conversion to collect the pydantic json schema details in

Types of Changes

  • Core
  • Bugfix
  • New feature
  • Enhancement/optimization
  • Documentation

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • I have tested the changes and verified that they work and don't break anything (as well as I can manage).

Summary by Sourcery

Fix Pydantic generic model support in Strawberry conversion and introduce json_schema_directive to annotate fields with Pydantic JSON schema metadata as GraphQL directives

New Features:

  • Add json_schema_directive option to Pydantic type and input decorators to map Pydantic JSON schema extras into GraphQL field directives

Bug Fixes:

  • Enable correct conversion of Pydantic models with generics into Strawberry types

Enhancements:

  • Extend type replacement logic to handle Pydantic generic metadata when resolving field types
  • Preserve original generic bases during dataclass creation for Strawberry types

Documentation:

  • Update documentation to describe json_schema_directive usage

Tests:

  • Add tests for nested Pydantic generics resolution
  • Add tests for applying JSON schema directives on fields

Copy link
Contributor

sourcery-ai bot commented Sep 23, 2025

Reviewer's Guide

Fix Pydantic generics conversion by handling generic metadata and preserving type parameters in dataclass creation, and add support for attaching schema directives based on Pydantic JSON schema extras. Decorator signatures are extended to accept a json_schema_directive, with extraction and application of JSON schema properties to field directives. New tests cover nested generic models and directive-based schema integration.

File-Level Changes

Change Details Files
Handle Pydantic generics correctly during type replacement and dataclass creation
  • Extend replace_types_recursively to detect and apply pydantic_generic_metadata args
  • Use orig_bases when making dataclasses to preserve Generic bases
strawberry/experimental/pydantic/fields.py
strawberry/experimental/pydantic/object_type.py
Integrate JSON schema directives into Pydantic conversion
  • Add json_schema_directive parameter to type and input decorators
  • Extract field JSON schema extras via model_json_schema and attach directive instances in _build_dataclass_creation_fields
strawberry/experimental/pydantic/object_type.py
Add tests for nested generic Pydantic models
  • Introduce test_nested_type_with_resolved_generic
  • Introduce test_nested_type_with_resolved_field_generic
tests/experimental/pydantic/schema/test_basic.py
Add tests for directive-based JSON schema integration
  • Add test_basic_type_field_list in new test_directives.py
tests/experimental/pydantic/schema/test_directives.py

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

@botberry
Copy link
Member

botberry commented Sep 23, 2025

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


This release fixes the pydantic support for generics and allows capture of the
Pydantic JSON schema attributes through a schema directive.

Here's the tweet text:

🆕 Release (next) is out! Thanks to Boudewijn van Groos for the PR 👏

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

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 there - I've reviewed your changes - here's some feedback:

  • Extract the JSON schema directive application logic in _build_dataclass_creation_fields into a separate helper to reduce complexity and improve readability.
  • Relying on the internal __pydantic_generic_metadata__ to resolve generics is brittle – consider using official Pydantic or typing‐utilities APIs to retrieve generic arguments.
  • The call to model.model_json_schema() on every type wrap may be expensive for large models; consider caching or deferring it until directives are actually applied.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Extract the JSON schema directive application logic in `_build_dataclass_creation_fields` into a separate helper to reduce complexity and improve readability.
- Relying on the internal `__pydantic_generic_metadata__` to resolve generics is brittle – consider using official Pydantic or typing‐utilities APIs to retrieve generic arguments.
- The call to `model.model_json_schema()` on every type wrap may be expensive for large models; consider caching or deferring it until directives are actually applied.

## Individual Comments

### Comment 1
<location> `tests/experimental/pydantic/schema/test_basic.py:531-540` </location>
<code_context>
     assert result.data["user"]["age"] == 1
     assert result.data["user"]["password"] is None

+def test_nested_type_with_resolved_generic():
+
+    A = TypeVar("A")
+    class Hobby(pydantic.BaseModel, Generic[A]):
+        name: A
+
+    @strawberry.experimental.pydantic.type(Hobby)
+    class HobbyType(Generic[A]):
+        name: strawberry.auto
+
+    class User(pydantic.BaseModel):
+        hobby: Hobby[str]
+
+    @strawberry.experimental.pydantic.type(User)
+    class UserType:
+        hobby: strawberry.auto
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def user(self) -> UserType:
+            return UserType(hobby=HobbyType(name="Skii"))
+
+    schema = strawberry.Schema(query=Query)
+
+    query = "{ user { hobby { name } } }"
+
+    result = schema.execute_sync(query)
+
+    assert not result.errors
+    assert result.data["user"]["hobby"]["name"] == "Skii"
+
+def test_nested_type_with_resolved_field_generic():
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding tests for error conditions and invalid generic types.

Please add tests for cases where the generic type is missing, invalid, or mismatched to verify error handling and type resolution.
</issue_to_address>

### Comment 2
<location> `tests/experimental/pydantic/schema/test_basic.py:563-594` </location>
<code_context>
+    assert not result.errors
+    assert result.data["user"]["hobby"]["name"] == "Skii"
+
+def test_nested_type_with_resolved_field_generic():
+    Count: TypeAlias = Annotated[float, pydantic.Field(ge = 0)]
+
+    A = TypeVar("A")
+    class Hobby(pydantic.BaseModel, Generic[A]):
+        count: A
+
+    @strawberry.experimental.pydantic.type(Hobby)
+    class HobbyType(Generic[A]):
+        count: strawberry.auto
+
+    class User(pydantic.BaseModel):
+        hobby: Hobby[Count]
+
+    @strawberry.experimental.pydantic.type(User)
+    class UserType:
+        hobby: strawberry.auto
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def user(self) -> UserType:
+            return UserType(hobby=HobbyType(count=2))
+
+    schema = strawberry.Schema(query=Query)
+
+    query = "{ user { hobby { count } } }"
+
+    result = schema.execute_sync(query)
+
+    assert not result.errors
+    assert result.data["user"]["hobby"]["count"] == 2
+

</code_context>

<issue_to_address>
**suggestion (testing):** Add assertions for field constraints and edge cases.

Consider adding tests that pass invalid values (such as negative numbers) to ensure the constraint is enforced and errors are reported correctly.

```suggestion
def test_nested_type_with_resolved_field_generic():
    Count: TypeAlias = Annotated[float, pydantic.Field(ge = 0)]

    A = TypeVar("A")
    class Hobby(pydantic.BaseModel, Generic[A]):
        count: A

    @strawberry.experimental.pydantic.type(Hobby)
    class HobbyType(Generic[A]):
        count: strawberry.auto

    class User(pydantic.BaseModel):
        hobby: Hobby[Count]

    @strawberry.experimental.pydantic.type(User)
    class UserType:
        hobby: strawberry.auto

    @strawberry.type
    class Query:
        @strawberry.field
        def user(self) -> UserType:
            return UserType(hobby=HobbyType(count=2))

    schema = strawberry.Schema(query=Query)

    query = "{ user { hobby { count } } }"

    result = schema.execute_sync(query)

    assert not result.errors
    assert result.data["user"]["hobby"]["count"] == 2

def test_nested_type_with_resolved_field_generic_constraint_violation():
    Count: TypeAlias = Annotated[float, pydantic.Field(ge = 0)]

    A = TypeVar("A")
    class Hobby(pydantic.BaseModel, Generic[A]):
        count: A

    @strawberry.experimental.pydantic.type(Hobby)
    class HobbyType(Generic[A]):
        count: strawberry.auto

    class User(pydantic.BaseModel):
        hobby: Hobby[Count]

    @strawberry.experimental.pydantic.type(User)
    class UserType:
        hobby: strawberry.auto

    @strawberry.type
    class Query:
        @strawberry.field
        def user(self) -> UserType:
            # Intentionally pass a negative value to violate the constraint
            return UserType(hobby=HobbyType(count=-1))

    schema = strawberry.Schema(query=Query)

    query = "{ user { hobby { count } } }"

    result = schema.execute_sync(query)

    assert result.errors, "Expected errors due to constraint violation"
    assert result.data is None or result.data["user"]["hobby"]["count"] is None
```
</issue_to_address>

### Comment 3
<location> `tests/experimental/pydantic/schema/test_directives.py:8` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Don't import test modules. ([`dont-import-test-modules`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/dont-import-test-modules))

<details><summary>Explanation</summary>Don't import test modules.

Tests should be self-contained and don't depend on each other.

If a helper function is used by multiple tests,
define it in a helper module,
instead of importing one test from the other.
</details>
</issue_to_address>

### Comment 4
<location> `strawberry/experimental/pydantic/object_type.py:95-100` </location>
<code_context>
def _build_dataclass_creation_fields(
    field: CompatModelField,
    is_input: bool,
    existing_fields: dict[str, StrawberryField],
    auto_fields_set: set[str],
    use_pydantic_alias: bool,
    compat: PydanticCompat,
    json_schema: dict[str, Any],
    json_schema_directive: Optional[builtins.type] = None,
) -> DataclassCreationFields:
    field_type = (
        get_type_for_field(field, is_input, compat=compat)
        if field.name in auto_fields_set
        else existing_fields[field.name].type
    )

    if (
        field.name in existing_fields
        and existing_fields[field.name].base_resolver is not None
    ):
        # if the user has defined a resolver for this field, always use it
        strawberry_field = existing_fields[field.name]
    else:
        # otherwise we build an appropriate strawberry field that resolves it
        existing_field = existing_fields.get(field.name)
        graphql_name = None
        if existing_field and existing_field.graphql_name:
            graphql_name = existing_field.graphql_name
        elif field.has_alias and use_pydantic_alias:
            graphql_name = field.alias

        if json_schema_directive and json_schema:
            field_names = {
                field.name for field in dataclasses.fields(json_schema_directive)
            }
            applicable_values = {
                    key: value
                    for key, value in json_schema.items()
                    if key in field_names
                }
            if applicable_values:
                json_directive = json_schema_directive(
                    **applicable_values
                )
                directives = (
                    *(existing_field.directives if existing_field else ()),
                    json_directive,
                )
            else:
                directives = existing_field.directives if existing_field else ()
        else:
            directives = ()

        strawberry_field = StrawberryField(
            python_name=field.name,
            graphql_name=graphql_name,
            # always unset because we use default_factory instead
            default=dataclasses.MISSING,
            default_factory=get_default_factory_for_field(field, compat=compat),
            type_annotation=StrawberryAnnotation.from_annotation(field_type),
            description=field.description,
            deprecation_reason=(
                existing_field.deprecation_reason if existing_field else None
            ),
            permission_classes=(
                existing_field.permission_classes if existing_field else []
            ),
            directives=directives,
            metadata=existing_field.metadata if existing_field else {},
        )

    return DataclassCreationFields(
        name=field.name,
        field_type=field_type,  # type: ignore
        field=strawberry_field,
    )

</code_context>

<issue_to_address>
**suggestion (code-quality):** We've found these issues:

- Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
- Low code quality found in \_build\_dataclass\_creation\_fields - 19% ([`low-code-quality`](https://docs.sourcery.ai/Reference/Default-Rules/comments/low-code-quality/))

```suggestion
            if applicable_values := {
                key: value
                for key, value in json_schema.items()
                if key in field_names
            }:
```

<br/><details><summary>Explanation</summary>
The quality score for this function is below the quality threshold of 25%.
This score is a combination of the method length, cognitive complexity and working memory.

How can you solve this?

It might be worth refactoring this function to make it shorter and more readable.

- Reduce the function length by extracting pieces of functionality out into
  their own functions. This is the most important thing you can do - ideally a
  function should be less than 10 lines.
- Reduce nesting, perhaps by introducing guard clauses to return early.
- Ensure that variables are tightly scoped, so that code using related concepts
  sits together within the function rather than being scattered.</details>
</issue_to_address>

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.

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.

Greptile Overview

Summary

This PR fixes two important issues in Strawberry's Pydantic integration: generic type conversion and adds JSON schema directive support.

Key Changes:

  • Generic Type Fix: Enhanced replace_types_recursively in fields.py to properly handle Pydantic generic models by checking __pydantic_generic_metadata__ and applying type arguments correctly
  • JSON Schema Directives: Added json_schema_directive parameter to @pydantic.type() and @pydantic.input() decorators, enabling automatic application of schema directives based on Pydantic field metadata
  • Generic Base Handling: Fixed dataclass creation to use __orig_bases__ instead of __bases__ for proper generic inheritance
  • Comprehensive Testing: Added tests for both generic type resolution scenarios and JSON schema directive functionality

The implementation is solid with proper error handling and follows existing code patterns. The JSON schema directive feature integrates cleanly with Strawberry's directive system, automatically extracting relevant metadata from Pydantic's json_schema_extra and applying it as GraphQL directives.

Confidence Score: 4/5

  • This PR is safe to merge with good test coverage and minimal risk
  • Score reflects solid implementation with comprehensive tests, but minor improvement suggested for error handling in generic type processing
  • Pay attention to test_generic.py which is empty and should contain generic-specific tests

Important Files Changed

File Analysis

Filename        Score        Overview
strawberry/experimental/pydantic/fields.py 4/5 Fixed generic type handling by checking for __pydantic_generic_metadata__ and applying args correctly
strawberry/experimental/pydantic/object_type.py 4/5 Added json schema directive support and fixed generic bases handling using __orig_bases__
tests/experimental/pydantic/schema/test_generic.py 3/5 Empty test file that should contain generic-specific tests

Sequence Diagram

sequenceDiagram
    participant U as User Code
    participant PT as @pydantic.type decorator
    participant RTR as replace_types_recursively
    participant BDCF as _build_dataclass_creation_fields
    participant SM as Strawberry Model

    U->>PT: Define Pydantic model with generics
    PT->>RTR: Process field types
    
    alt Pydantic Generic detected
        RTR->>RTR: Check __pydantic_generic_metadata__
        RTR->>RTR: Apply generic args to replaced_type
    else Regular type
        RTR->>RTR: Return replaced_type as-is
    end
    
    RTR->>BDCF: Return processed type
    
    opt JSON Schema Directive provided
        BDCF->>BDCF: Extract field schema properties
        BDCF->>BDCF: Create directive instance
        BDCF->>BDCF: Add to field directives
    end
    
    BDCF->>PT: Return field configuration
    PT->>PT: Use __orig_bases__ for generic inheritance
    PT->>SM: Create Strawberry dataclass
    SM->>U: Return configured type
Loading

5 files reviewed, 2 comments

Edit Code Review Bot Settings | Greptile

Comment on lines 49 to 50
if hasattr(basic_type, "__pydantic_generic_metadata__") and basic_type.__pydantic_generic_metadata__["args"]:
return replaced_type[*basic_type.__pydantic_generic_metadata__["args"]]
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider checking if args exists and is not empty to avoid potential KeyError

Suggested change
if hasattr(basic_type, "__pydantic_generic_metadata__") and basic_type.__pydantic_generic_metadata__["args"]:
return replaced_type[*basic_type.__pydantic_generic_metadata__["args"]]
if hasattr(basic_type, "__pydantic_generic_metadata__") and basic_type.__pydantic_generic_metadata__.get("args"):
return replaced_type[*basic_type.__pydantic_generic_metadata__["args"]]

@Boudewijn26 Boudewijn26 force-pushed the feature/improved-pydantics branch from 048ecf2 to 65beaaf Compare September 23, 2025 13:06
schema = strawberry.Schema(query=Query)

expected_schema = """
directive @jsonSchema(test: Int!, exclusiveMinimum: Int = null) on FIELD_DEFINITION
Copy link
Member

Choose a reason for hiding this comment

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

Would this also include values from other fields on other types?

And if so, would this become too bloated in case we have N fields, each with M different extras?

I'm not much familiar with the pydantic integration here, so @patrick91 may be the best to check this :)

Copy link
Author

Choose a reason for hiding this comment

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

Only the values in the json schema which are present on the passed directive are passed to the graphQL schema.

So bloat would be limited by whichever values the user wants in their directive. They could also decide to have different directives for different types to prevent their directives from growing too much

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.

3 participants