First-class Pydantic v2+ support - #3965
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:
|
|
/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 |
| 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.
| 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] |
There was a problem hiding this comment.
this might return something_else instead of somethingElse
There was a problem hiding this comment.
what if there's no location? (for root errors)
There was a problem hiding this comment.
|
|
||
|
|
||
| @strawberry.pydantic.type | ||
| class User(BaseModel): |
There was a problem hiding this comment.
| class User(BaseModel): | |
| class User(Node): |
| name: str = Field(alias="fullName") | ||
| age: int = Field(alias="yearsOld") |
There was a problem hiding this comment.
| 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.
| ### 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.
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.
| ### 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") |
There was a problem hiding this comment.
This is Pydantic v1, we should use field_validator
Let's also maybe use number: Annotated[int, AfterValidator(is_even)]
| ## 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.
| ## 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 |
| 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 |
| """Represents a single validation error detail.""" | ||
|
|
||
| type: str | ||
| loc: list[str] |
There was a problem hiding this comment.
| loc: list[str] | |
| location: list[str] |
|
|
||
| type: str | ||
| loc: list[str] | ||
| msg: str |
| """ | ||
| return Error( | ||
| errors=[ | ||
| ErrorDetail( |
There was a problem hiding this comment.
| 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.
reuse the one from strawberry/types/object_type.py
| age: int | ||
| password: Optional[str] | ||
|
|
||
| definition: StrawberryObjectDefinition = User.__strawberry_definition__ |
There was a problem hiding this comment.
let's use the util to get the definition
7067816 to
33ab45c
Compare
|
Hi, thanks for contributing to this project! We noticed that this PR is missing a So as soon as this PR is merged, a release will be made 🚀. Here's an example of ---
release type: patch
---
Description of the changes, ideally with some examples, if adding a new feature.
Release type can be one of patch, minor or major. We use [semver](https://semver.org/), so make sure to pick the appropriate type. If in doubt feel free to ask :) |
Apollo Federation Subgraph Compatibility Results
Learn more: |
2501274 to
aa3b94d
Compare
31bc6c3 to
222d5ec
Compare
f1d17fe to
32c8d2e
Compare
e6cf28e to
700155a
Compare
0f8436b to
88ccd0b
Compare
700155a to
2a857ac
Compare
a0c20b8 to
a45e381
Compare
2a857ac to
2ed8272
Compare
af2fd79 to
f052e20
Compare
2ed8272 to
d0c2d3b
Compare
f052e20 to
ddaebe9
Compare
d0c2d3b to
62d8f77
Compare
ddaebe9 to
9bac2f2
Compare
62d8f77 to
a39a9fd
Compare
9bac2f2 to
6d38576
Compare
a39a9fd to
4b478be
Compare
6d38576 to
5aa1c40
Compare
4b478be to
fb74bb0
Compare
5aa1c40 to
8ae4951
Compare
fb74bb0 to
799c41e
Compare
8ae4951 to
9ed2d87
Compare
02240e4 to
028fa4d
Compare
Shortcake-Parent: main
for more information, see https://pre-commit.ci
9ed2d87 to
70b518c
Compare

Stack 🍰
feature/pydantic-first-class) <-- this PR2026-06-28-add-generic-exception-handlers)2026-07-06-unpack-input-mutation-arguments)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