-
-
Notifications
You must be signed in to change notification settings - Fork 602
First-class Pydantic v2+ support #3965
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Reviewer's GuideThis 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
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3965 +/- ##
==========================================
- Coverage 94.41% 91.27% -3.14%
==========================================
Files 536 563 +27
Lines 35036 38186 +3150
Branches 1842 1913 +71
==========================================
+ Hits 33079 34856 +1777
- Misses 1659 3023 +1364
- Partials 298 307 +9 🚀 New features to boost your workflow:
|
CodSpeed Performance ReportMerging #3965 will not alter performanceComparing Summary
|
|
/pre-release |
Pre-release👋 Pre-release 0.279.0.dev.1754159379 [70130b4] has been released on PyPi! 🚀 poetry add strawberry-graphql==0.279.0.dev.1754159379 |
|
/pre-release |
|
/pre-release |
tests/pydantic/test_error.py
Outdated
| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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" | |
| ) | |
for more information, see https://pre-commit.ci
| """Represents a single validation error detail.""" | ||
|
|
||
| type: str | ||
| loc: list[str] |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| class User(BaseModel): | |
| class User(Node): |
| name: str = Field(alias="fullName") | ||
| age: int = Field(alias="yearsOld") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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?
| ### 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 | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| ### 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 | |
| ``` |
| if user.password: | ||
| return user | ||
| return None |
There was a problem hiding this comment.
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
| ### 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)] | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| ### 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)] | |
| ``` |
docs/integrations/pydantic.md
Outdated
| name: str | ||
| age: int | ||
|
|
||
| @validator("age") |
There was a problem hiding this comment.
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)]
docs/integrations/pydantic.md
Outdated
| ## 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() | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| ## 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove this
| 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 |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| loc: list[str] | |
| location: list[str] |
|
|
||
| type: str | ||
| loc: list[str] | ||
| msg: str |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| """ | ||
| return Error( | ||
| errors=[ | ||
| ErrorDetail( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| ErrorDetail( | |
| ErrorDetail.from_error |
| 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 |
There was a problem hiding this comment.
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
tests/pydantic/test_basic.py
Outdated
| age: int | ||
| password: Optional[str] | ||
|
|
||
| definition: StrawberryObjectDefinition = User.__strawberry_definition__ |
There was a problem hiding this comment.
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
- Functional validators (BeforeValidator, AfterValidator, WrapValidator) - Model validators with mode='before'|'after'|'wrap' - Validation context passing Strawberry Info to validators - model_config settings (strict, extra, from_attributes) - Per-field strict mode via Field(strict=True) - Separate validation/serialization aliases - Discriminated unions with Literal type support - TypeAdapter and RootModel support in resolvers - Updated documentation with Pydantic v2 examples - Added 9 new test files for comprehensive coverage (130 tests) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
for more information, see https://pre-commit.ci

Summary
Add first-class support for Pydantic v2+ models in Strawberry GraphQL.
This PR introduces a new
strawberry.pydanticmodule that allows you to directly decorate PydanticBaseModelclasses to create GraphQL types, inputs, and interfaces without requiring separate wrapper classes.Basic Usage
Features Implemented
Core Features
@strawberry.pydantic.type- Convert Pydantic models to GraphQL types@strawberry.pydantic.input- Convert Pydantic models to GraphQL input types@strawberry.pydantic.interface- Convert Pydantic models to GraphQL interfacesstrawberry.Privateto exclude fields from schemastrawberry.field()withAnnotatedfor directives, permissions, deprecationstrawberry.pydantic.Errortype for validation error handlinginclude_computed=TruePydantic v2 Features (130 Tests Passing)
BeforeValidator,AfterValidator,WrapValidatorwithAnnotatedtypesmode='before'|'after'|'wrap'Infoautomatically passed to Pydantic validatorsstrict=True,extra='forbid',from_attributes=Trueall respectedField(strict=True)for individual field validationvalidation_aliasandserialization_aliassupported viaby_name=TrueLiteraltype support for type discriminatorsTypeAdapterin resolvers for scalar/list validationRootModelfor validated list/dict wrappers in resolversMigration from Experimental
Validation Context Example
Pydantic validators can access GraphQL context for permission-based validation:
Discriminated Union Example
Test Results
🤖 Generated with Claude Code