Skip to content

Commit 29a1b91

Browse files
committed
fix: ignore null fields
1 parent 64754d9 commit 29a1b91

File tree

3 files changed

+35
-1
lines changed

3 files changed

+35
-1
lines changed

MIGRATION_v2_to_v3.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,26 @@ The general renaming rules:
203203
| `MembershipLevel` | `MembershipLevelResponse` | |
204204
| `ThreadedComment` | `ThreadedCommentResponse` | |
205205

206+
## JSON Serialization of Optional Fields
207+
208+
Optional fields in request objects are now omitted from the JSON body when not set, instead of being sent as explicit `null`. Previously, every unset field was serialized as `null`, which caused the backend to zero out existing values on partial updates.
209+
210+
**Before:**
211+
```python
212+
client.update_app(enforce_unique_usernames="no")
213+
# Wire: {"enforce_unique_usernames":"no","webhook_url":null,"multi_tenant_enabled":null,...}
214+
# Backend: sets enforce_unique_usernames="no", but ALSO resets webhook_url="", multi_tenant_enabled=false, etc.
215+
```
216+
217+
**After:**
218+
```python
219+
client.update_app(enforce_unique_usernames="no")
220+
# Wire: {"enforce_unique_usernames":"no"}
221+
# Backend: sets enforce_unique_usernames="no", all other fields preserved
222+
```
223+
224+
List and dict fields are still serialized when set (including as empty `[]`/`{}`), so you can continue to send an empty list to clear a list field. Unset collection fields (`None`) are now also omitted.
225+
206226
## Getting Help
207227

208228
- [Stream documentation](https://getstream.io/docs/)

getstream/base.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@
2525
import ijson
2626

2727

28+
def _strip_none(obj):
29+
"""Recursively remove None values from dicts so unset optional fields
30+
are omitted from the JSON body instead of being sent as null."""
31+
if isinstance(obj, dict):
32+
return {k: _strip_none(v) for k, v in obj.items() if v is not None}
33+
if isinstance(obj, list):
34+
return [_strip_none(item) for item in obj]
35+
return obj
36+
37+
2838
def build_path(path: str, path_params: Optional[Dict[str, Any]]) -> str:
2939
if path_params is None:
3040
return path
@@ -169,6 +179,8 @@ def _request_sync(
169179
data_type: Optional[Type[T]] = None,
170180
):
171181
kwargs = kwargs or {}
182+
if "json" in kwargs and kwargs["json"] is not None:
183+
kwargs["json"] = _strip_none(kwargs["json"])
172184
url_path, url_full, endpoint, attrs = self._prepare_request(
173185
method, path, query_params, kwargs
174186
)
@@ -348,6 +360,8 @@ async def _request_async(
348360
data_type: Optional[Type[T]] = None,
349361
):
350362
kwargs = kwargs or {}
363+
if "json" in kwargs and kwargs["json"] is not None:
364+
kwargs["json"] = _strip_none(kwargs["json"])
351365
query_params = query_params or {}
352366
url_path, url_full, endpoint, attrs = self._prepare_request(
353367
method, path, query_params, kwargs

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)