Skip to content

Commit edb8348

Browse files
authored
Change Maybe to Some[T] | None instead of Some[T | None] | None (#3961)
* Change `Maybe` to `Some[T] | None` instead of `Some[T | None] | None` * Codemod * Update tests * Fix type * Fix * Fix pipe * Docs * Make sure we can't pass null * Use validation rules * Apply suggestion from @patrick91 * Add release notes
1 parent f48434d commit edb8348

20 files changed

Lines changed: 1954 additions & 101 deletions

File tree

RELEASE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Release type: minor
2+
3+
This release changes the `strawberry.Maybe` type to provide a more consistent and intuitive API for handling optional fields in GraphQL inputs.
4+
5+
**Breaking Change**: The `Maybe` type definition has been changed from `Union[Some[Union[T, None]], None]` to `Union[Some[T], None]`. This means:
6+
7+
- `Maybe[str]` now only accepts string values or absent fields (refuses explicit null)
8+
- `Maybe[str | None]` accepts strings, null, or absent fields (maintains previous behavior)
9+
10+
This provides a cleaner API where `if field is not None` consistently means "field was provided" for all Maybe fields. A codemod is available to automatically migrate your code: `strawberry upgrade maybe-optional`
11+
12+
See the [breaking changes documentation](https://strawberry.rocks/docs/breaking-changes/0.279.0) for migration details.

docs/breaking-changes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ title: List of breaking changes and deprecations
44

55
# List of breaking changes and deprecations
66

7+
- [Version 0.279.0 - 19 August 2025](./breaking-changes/0.279.0.md)
78
- [Version 0.268.0 - 10 May 2025](./breaking-changes/0.268.0.md)
89
- [Version 0.249.0 - 18 November 2024](./breaking-changes/0.249.0.md)
910
- [Version 0.243.0 - 25 September 2024](./breaking-changes/0.243.0.md)

docs/breaking-changes/0.279.0.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
---
2+
title: 0.279.0 Breaking Changes
3+
slug: breaking-changes/0.279.0
4+
---
5+
6+
# v0.279.0 Breaking Changes
7+
8+
This release changes the `strawberry.Maybe` type definition to provide a more
9+
consistent and intuitive API for handling optional fields.
10+
11+
## What Changed
12+
13+
The `Maybe` type definition has been changed from:
14+
15+
```python
16+
Maybe: TypeAlias = Union[Some[Union[T, None]], None]
17+
```
18+
19+
to:
20+
21+
```python
22+
Maybe: TypeAlias = Union[Some[T], None]
23+
```
24+
25+
## Impact on Your Code
26+
27+
### Type Annotations
28+
29+
If you were using `Maybe[T]` and expecting to handle explicit `null` values, you
30+
now need to explicitly declare this with `Maybe[T | None]`:
31+
32+
```python
33+
# Before (0.278.0 and earlier)
34+
field: strawberry.Maybe[str] # Could handle Some(None)
35+
36+
# After (0.279.0+)
37+
field: strawberry.Maybe[str] # Only handles Some("value") or None (absent)
38+
field: strawberry.Maybe[str | None] # Handles Some("value"), Some(None), or None
39+
```
40+
41+
### Runtime Behavior
42+
43+
The runtime behavior changes to provide more consistent field checking:
44+
45+
- `Maybe[str]` now represents "field present with non-null value" or "field
46+
absent"
47+
- `Maybe[str | None]` represents "field present with value", "field present but
48+
null", or "field absent"
49+
50+
This means `Maybe[str]` can no longer receive explicit `null` values - they will
51+
cause a validation error.
52+
53+
### Consistent Field Checking
54+
55+
This change provides a single, consistent way to check if a field was provided,
56+
regardless of whether the field allows null values:
57+
58+
```python
59+
@strawberry.input
60+
class UpdateUserInput:
61+
# Can be provided with a value or not provided at all
62+
name: strawberry.Maybe[str]
63+
64+
# Can be provided with a value, provided as null, or not provided at all
65+
phone: strawberry.Maybe[str | None]
66+
67+
68+
@strawberry.mutation
69+
def update_user(input: UpdateUserInput) -> User:
70+
# Same checking pattern for both fields
71+
if input.name is not None: # Field was provided
72+
user.name = input.name.value # Type checker knows this is str
73+
74+
if input.phone is not None: # Field was provided
75+
user.phone = input.phone.value # Type checker knows this is str | None
76+
```
77+
78+
The key benefit is that `if field is not None` now consistently means "field was
79+
provided" for all Maybe fields.
80+
81+
## Migration
82+
83+
### Automatic Migration
84+
85+
Strawberry provides a codemod to automatically update your code:
86+
87+
```bash
88+
strawberry upgrade maybe-optional
89+
```
90+
91+
The codemod will automatically convert `Maybe[T]` to `Maybe[T | None]` to
92+
maintain the previous behavior.
93+
94+
### Manual Migration
95+
96+
Review your `Maybe` usage and decide whether you need the union type:
97+
98+
```python
99+
# If you only need "present" vs "absent" (most common case)
100+
field: strawberry.Maybe[str]
101+
102+
# If you need "present with value", "present but null", and "absent"
103+
field: strawberry.Maybe[str | None]
104+
```
105+
106+
## Why This Change?
107+
108+
After extensive discussion, we decided that changing `Maybe` to `Some[T] | None`
109+
(instead of `Some[T | None] | None`) provides the best developer experience
110+
because:
111+
112+
1. **Consistent field checking**: You can always use `if field is not None:` to
113+
check if a field was provided, regardless of whether the field allows null
114+
values
115+
116+
2. **Preserves existing behavior**: You can still get the previous behavior by
117+
using `Maybe[T | None]` when you need to handle explicit nulls
118+
119+
3. **Better type safety**: `Maybe[str]` now properly indicates that the field
120+
cannot be null, while `Maybe[str | None]` explicitly allows nulls
121+
122+
4. **Cleaner API**: There's now "one true way" to check if a field was provided,
123+
making the API more intuitive
124+
125+
The new approach provides both simplicity for common cases and flexibility for
126+
complex scenarios where null handling is needed.
127+
128+
## Need Help?
129+
130+
- See the [Maybe documentation](../types/maybe.md) for comprehensive usage
131+
examples
132+
- Check the
133+
[migration guide](../types/maybe.md#migration-from-previous-versions) for
134+
detailed migration instructions
135+
- If you encounter issues, please
136+
[report them on GitHub](https://github.com/strawberry-graphql/strawberry/issues)

docs/types/input-types.md

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -84,42 +84,10 @@ type Point2D {
8484

8585
</CodeGrid>
8686

87-
When you need to distinguish between a field being set to `null` or its value
88-
and the field being absent, optional arguments would not suffice. In this case
89-
you can use `strawberry.Maybe` like so:
90-
91-
<CodeGrid>
92-
93-
```python
94-
import strawberry
95-
from typing import Optional
96-
97-
98-
@strawberry.input
99-
class UpdateUserInput:
100-
name: str | None = None
101-
phone: strawberry.Maybe[str]
102-
103-
104-
@strawberry.type
105-
class Mutation:
106-
def update_user(self, user_id: strawberry.ID, input: UpdateUserInput) -> User:
107-
if name := input.name:
108-
... # update name...
109-
if input.phone:
110-
phone = input.phone.value # can be str | None
111-
... # update phone...
112-
```
113-
114-
```graphql
115-
type UpdateUserInput {
116-
name: String = null
117-
phone: String
118-
119-
}
120-
```
121-
122-
</CodeGrid>
87+
When you need to distinguish between a field being set to `null` versus being
88+
completely absent (common in update operations), you can use `strawberry.Maybe`.
89+
See the [Maybe documentation](./maybe.md) for comprehensive examples and usage
90+
patterns.
12391

12492
## API
12593

@@ -162,6 +130,14 @@ input SearchBy @oneOf {
162130

163131
</CodeGrid>
164132

133+
<Note>
134+
135+
OneOf inputs use `strawberry.Maybe` to distinguish between fields that are
136+
explicitly not provided versus those that might be set to null. See the
137+
[Maybe documentation](./maybe.md) for more details on this usage pattern.
138+
139+
</Note>
140+
165141
## Deprecating fields
166142

167143
Fields can be deprecated using the argument `deprecation_reason`.
@@ -188,15 +164,15 @@ class Point2D:
188164
z: Optional[float] = strawberry.field(
189165
deprecation_reason="3D coordinates are deprecated"
190166
)
191-
label: strawberry.Maybe[str]
167+
label: Optional[str] = None
192168
```
193169

194170
```graphql
195171
input Point2D {
196172
x: Float!
197173
y: Float!
198174
z: Float @deprecated(reason: "3D coordinates are deprecated")
199-
label: String
175+
label: String = null
200176
}
201177
```
202178

0 commit comments

Comments
 (0)