Skip to content

Conversation

patrick91
Copy link
Member

@patrick91 patrick91 commented Aug 2, 2025

  • Initial version implemented by claude
  • Private
  • Remove unused code and update plan
  • Remove unused flags
  • Split tests and more tests
  • Lint
  • Remove _strawberry_input_type

Summary by Sourcery

Implement first-class Pydantic support in Strawberry GraphQL by introducing dedicated decorators and utilities in a new pydantic module, fully replacing the experimental integration, enhancing schema generation and runtime behavior, enriching documentation, and adding an extensive test suite.

New Features:

  • Provide first-class Pydantic integration via new @strawberry.pydantic decorators for object types, inputs, and interfaces
  • Automatically generate GraphQL types from Pydantic BaseModel classes with full support for descriptions, aliases, optional and nested fields, and private attributes
  • Add conversion methods (from_pydantic, to_pydantic) and proper type resolution (is_type_of) for Pydantic-based GraphQL types

Enhancements:

  • Deprecate and remove the experimental Pydantic API in favor of the new first-class integration
  • Clean up unused flags and internal helpers, and update exports in the main strawberry module

Documentation:

  • Expand Pydantic integration guide in docs/integrations/pydantic.md with installation instructions, usage examples, configuration options, and a migration path from the experimental API

Tests:

  • Add comprehensive tests under tests/pydantic covering basic type definitions, schema execution, input validation, nested types, queries, mutations, private fields, and async support

Chores:

  • Introduce CLAUDE.md and PLAN.md for development guidance and completed implementation plan

Copy link
Contributor

sourcery-ai bot commented Aug 2, 2025

Reviewer's Guide

This PR introduces first-class Pydantic support by adding a dedicated strawberry/pydantic module with type/input/interface decorators, overhauls the Pydantic integration documentation and migration guide, cleans up experimental code and unused flags, and restructures and extends the test suite to cover all new features.

File-Level Changes

Change Details Files
Add first-class Pydantic integration decorators
  • Create strawberry/pydantic/object_type.py with processing logic and decorators
  • Implement _get_pydantic_fields and conversion utilities in strawberry/pydantic/fields.py
  • Export new pydantic module in strawberry/pydantic/init.py
  • Update strawberry/init.py to include pydantic in the public API
strawberry/pydantic/object_type.py
strawberry/pydantic/fields.py
strawberry/pydantic/__init__.py
strawberry/__init__.py
Overhaul Pydantic documentation and remove experimental section
  • Expand docs/integrations/pydantic.md with installation, usage examples, decorator reference, advanced scenarios, and migration guide
  • Remove outdated experimental examples and flags
docs/integrations/pydantic.md
Restructure and extend test suite for Pydantic integration
  • Move experimental tests into a new tests/pydantic directory and split them into focused modules
  • Add comprehensive tests covering basic types, execution (queries/mutations/async), special features, nested types, and validation error handling
tests/pydantic/test_basic.py
tests/pydantic/test_execution.py
tests/pydantic/test_special_features.py
tests/pydantic/test_queries_mutations.py
tests/pydantic/test_nested_types.py
Cleanup unused experimental code and flags
  • Remove leftover experimental flags and references from strawberry.experimental.pydantic
  • Delete deprecated _strawberry_input_type usage
  • Tidy up compat module imports
strawberry/experimental/pydantic/_compat.py
Update CLAUDE.md and PLAN.md with new integration status
  • Detail completed implementation steps and migration path in PLAN.md
  • Provide guidance and commands for the repository in CLAUDE.md
PLAN.md
CLAUDE.md

Possibly linked issues

  • Add pytest action #1: The PR implements first-class Pydantic integration, including new decorators and updated documentation, directly addressing Pydantic V2 support and migration.

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

codecov bot commented Aug 2, 2025

Codecov Report

❌ Patch coverage is 87.67034% with 190 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.97%. Comparing base (1bbe8db) to head (34bfa44).
⚠️ Report is 14 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3965      +/-   ##
==========================================
- Coverage   94.40%   93.97%   -0.44%     
==========================================
  Files         528      549      +21     
  Lines       34371    36335    +1964     
  Branches     1803     1882      +79     
==========================================
+ Hits        32449    34144    +1695     
- Misses       1630     1880     +250     
- Partials      292      311      +19     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

codspeed-hq bot commented Aug 2, 2025

CodSpeed Performance Report

Merging #3965 will not alter performance

Comparing feature/pydantic-first-class (34bfa44) with main (0bdfea4)

Summary

✅ 26 untouched benchmarks

@patrick91
Copy link
Member Author

/pre-release

@botberry
Copy link
Member

botberry commented Aug 2, 2025

Pre-release

👋

Pre-release 0.279.0.dev.1754159379 [70130b4] has been released on PyPi! 🚀
You can try it by doing:

poetry add strawberry-graphql==0.279.0.dev.1754159379

@patrick91
Copy link
Member Author

/pre-release

@strawberry-graphql strawberry-graphql deleted a comment from botberry Aug 2, 2025
@patrick91
Copy link
Member Author

/pre-release

Comment on lines 95 to 103
try:
# Validate the input using Pydantic
validated = CreateUserModel(name=input.name, age=input.age)
# Simulate successful creation
return CreateUserSuccess(
user_id=1, message=f"User {validated.name} created successfully"
)
except pydantic.ValidationError as e:
return Error.from_validation_error(e)
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
try:
# Validate the input using Pydantic
validated = CreateUserModel(name=input.name, age=input.age)
# Simulate successful creation
return CreateUserSuccess(
user_id=1, message=f"User {validated.name} created successfully"
)
except pydantic.ValidationError as e:
return Error.from_validation_error(e)
# Validate the input using Pydantic
validated = CreateUserModel(name=input.name, age=input.age)
return CreateUserSuccess(
user_id=1, message=f"User {validated.name} created successfully"
)

"""Represents a single validation error detail."""

type: str
loc: list[str]
Copy link
Member Author

Choose a reason for hiding this comment

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

this might return something_else instead of somethingElse



@strawberry.pydantic.type
class User(BaseModel):
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
class User(BaseModel):
class User(Node):

Comment on lines +134 to +135
name: str = Field(alias="fullName")
age: int = Field(alias="yearsOld")
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
name: str = Field(alias="fullName")
age: int = Field(alias="yearsOld")
name: Annotated[str, Field(alias="fullName")]
age: Annotated[int, Field(alias="yearsOld")]

potentially also allow strawberry.pydantic.field?

Comment on lines +138 to +151
### Optional Fields

Pydantic optional fields are properly handled:

```python
from typing import Optional


@strawberry.pydantic.type
class User(BaseModel):
name: str
email: Optional[str] = None
age: Optional[int] = None
```
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
### Optional Fields
Pydantic optional fields are properly handled:
```python
from typing import Optional
@strawberry.pydantic.type
class User(BaseModel):
name: str
email: Optional[str] = None
age: Optional[int] = None
```

Comment on lines +190 to +192
if user.password:
return user
return None
Copy link
Member Author

Choose a reason for hiding this comment

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

this is a silly example, let's change it :D

Comment on lines 197 to 234
### Nested Types

Pydantic models can contain other Pydantic models:

```python
@strawberry.pydantic.type
class Address(BaseModel):
street: str
city: str
zipcode: str


@strawberry.pydantic.type
class User(BaseModel):
name: str
address: Address
```

### Lists and Collections

Lists of Pydantic models work seamlessly:

```python
from typing import List


@strawberry.pydantic.type
class User(BaseModel):
name: str
age: int


@strawberry.type
class Query:
@strawberry.field
def get_users(self) -> List[User]:
return [User(name="John", age=30), User(name="Jane", age=25)]
```
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
### Nested Types
Pydantic models can contain other Pydantic models:
```python
@strawberry.pydantic.type
class Address(BaseModel):
street: str
city: str
zipcode: str
@strawberry.pydantic.type
class User(BaseModel):
name: str
address: Address
```
### Lists and Collections
Lists of Pydantic models work seamlessly:
```python
from typing import List
@strawberry.pydantic.type
class User(BaseModel):
name: str
age: int
@strawberry.type
class Query:
@strawberry.field
def get_users(self) -> List[User]:
return [User(name="John", age=30), User(name="Jane", age=25)]
```

name: str
age: int

@validator("age")
Copy link
Member Author

Choose a reason for hiding this comment

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

This is Pydantic v1, we should use field_validator

Let's also maybe use number: Annotated[int, AfterValidator(is_even)]

Comment on lines +256 to +273
## Conversion Methods

Decorated models automatically get conversion methods:

```python
@strawberry.pydantic.type
class User(BaseModel):
name: str
age: int


# Create from existing Pydantic instance
pydantic_user = User(name="John", age=30)
strawberry_user = User.from_pydantic(pydantic_user)

# Convert back to Pydantic
converted_back = strawberry_user.to_pydantic()
```
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
## Conversion Methods
Decorated models automatically get conversion methods:
```python
@strawberry.pydantic.type
class User(BaseModel):
name: str
age: int
# Create from existing Pydantic instance
pydantic_user = User(name="John", age=30)
strawberry_user = User.from_pydantic(pydantic_user)
# Convert back to Pydantic
converted_back = strawberry_user.to_pydantic()
```

we don't even need this

converted_back = strawberry_user.to_pydantic()
```

## Migration from Experimental
Copy link
Member Author

Choose a reason for hiding this comment

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

remove this

Comment on lines +13 to +20
from .error import Error
from .object_type import input as input_decorator
from .object_type import interface
from .object_type import type as type_decorator

# Re-export with proper names
input = input_decorator
type = type_decorator
Copy link
Member Author

Choose a reason for hiding this comment

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

make this good

"""Represents a single validation error detail."""

type: str
loc: list[str]
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
loc: list[str]
location: list[str]


type: str
loc: list[str]
msg: str
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
msg: str
message: str

"""
return Error(
errors=[
ErrorDetail(
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
ErrorDetail(
ErrorDetail.from_error

Comment on lines +31 to +41
def _get_interfaces(cls: builtins.type[Any]) -> list[StrawberryObjectDefinition]:
"""Extract interfaces from a class's inheritance hierarchy."""
interfaces: list[StrawberryObjectDefinition] = []

for base in cls.__mro__[1:]: # Exclude current class
if hasattr(base, "__strawberry_definition__"):
type_definition = base.__strawberry_definition__
if type_definition.is_interface:
interfaces.append(type_definition)

return interfaces
Copy link
Member Author

Choose a reason for hiding this comment

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

reuse the one from strawberry/types/object_type.py

age: int
password: Optional[str]

definition: StrawberryObjectDefinition = User.__strawberry_definition__
Copy link
Member Author

Choose a reason for hiding this comment

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

let's use the util to get the definition

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants