Skip to content

Commit ca87b6c

Browse files
authored
[CHA-2716] Ignore null fields (#46)
* fix: ignore null fields * style: format code * test: add unit tests for null serialization
1 parent adde04f commit ca87b6c

File tree

9 files changed

+114
-38
lines changed

9 files changed

+114
-38
lines changed

MIGRATION_v5_to_v6.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,45 @@ New composition types: `HasOwnUser`, `HasUserCommonFields`, `HasUserPrivacyField
206206
| `MembershipLevel` | `MembershipLevelResponse` | |
207207
| `ThreadedComment` | `ThreadedCommentResponse` | |
208208

209+
## JSON Serialization of Optional Fields
210+
211+
Optional (nullable) 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.
212+
213+
**Before:**
214+
```java
215+
UpdateAppRequest request = UpdateAppRequest.builder()
216+
.enforceUniqueUsernames("no")
217+
.build();
218+
// Wire: {"enforce_unique_usernames":"no","webhook_url":null,"multi_tenant_enabled":null,...}
219+
// Backend: sets enforce_unique_usernames="no", but ALSO resets webhook_url="", multi_tenant_enabled=false, etc.
220+
```
221+
222+
**After:**
223+
```java
224+
UpdateAppRequest request = UpdateAppRequest.builder()
225+
.enforceUniqueUsernames("no")
226+
.build();
227+
// Wire: {"enforce_unique_usernames":"no"}
228+
// Backend: sets enforce_unique_usernames="no", all other fields preserved
229+
```
230+
231+
Collection fields (lists, maps) 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 (`null`) are now also omitted.
232+
233+
**Clearing individual fields:** To explicitly remove or reset a scalar field, use the partial update endpoints with the `unset` parameter instead of sending `null`:
234+
235+
```java
236+
// Clear custom fields from a user
237+
client.updateUsersPartial(UpdateUsersPartialRequest.builder()
238+
.users(List.of(UpdateUserPartialRequest.builder()
239+
.id(userId)
240+
.unset(List.of("field_to_clear"))
241+
.build()))
242+
.build())
243+
.execute();
244+
```
245+
246+
Partial update models (`UpdateUserPartialRequest`, `UpdateMessagePartialRequest`, `UpdateActivityPartialRequest`) support `set` (a map of fields to update) and `unset` (a list of field names to remove).
247+
209248
## Getting Help
210249

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

src/main/java/io/getstream/services/framework/StreamHTTPClient.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.getstream.services.framework;
22

3+
import com.fasterxml.jackson.annotation.JsonInclude;
34
import com.fasterxml.jackson.databind.DeserializationFeature;
45
import com.fasterxml.jackson.databind.ObjectMapper;
56
import com.fasterxml.jackson.databind.SerializationFeature;
@@ -33,6 +34,7 @@ public class StreamHTTPClient {
3334
private final ObjectMapper objectMapper =
3435
new ObjectMapper()
3536
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
37+
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
3638
.setDateFormat(
3739
new StdDateFormat()
3840
.withColonInTimeZone(true)

src/test/java/io/getstream/BasicTest.java

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -155,30 +155,20 @@ private static void createTestChannel() throws Exception {
155155
}
156156

157157
static void upsertUsers() throws Exception {
158+
String id1 = RandomStringUtils.randomAlphabetic(10);
159+
String id2 = RandomStringUtils.randomAlphabetic(10);
160+
String id3 = RandomStringUtils.randomAlphabetic(10);
161+
String id4 = RandomStringUtils.randomAlphabetic(10);
162+
158163
UserRequest testUserRequestObject =
159-
UserRequest.builder()
160-
.id(RandomStringUtils.randomAlphabetic(10))
161-
.name("Gandalf the Grey")
162-
.build();
164+
UserRequest.builder().id(id1).name("Gandalf " + id1).build();
163165

164166
List<UserRequest> testUsersRequestObjects = new ArrayList<>();
165167

166168
testUsersRequestObjects.add(testUserRequestObject);
167-
testUsersRequestObjects.add(
168-
UserRequest.builder()
169-
.id(RandomStringUtils.randomAlphabetic(10))
170-
.name("Frodo Baggins")
171-
.build());
172-
testUsersRequestObjects.add(
173-
UserRequest.builder()
174-
.id(RandomStringUtils.randomAlphabetic(10))
175-
.name("Frodo Baggins")
176-
.build());
177-
testUsersRequestObjects.add(
178-
UserRequest.builder()
179-
.id(RandomStringUtils.randomAlphabetic(10))
180-
.name("Samwise Gamgee")
181-
.build());
169+
testUsersRequestObjects.add(UserRequest.builder().id(id2).name("Frodo " + id2).build());
170+
testUsersRequestObjects.add(UserRequest.builder().id(id3).name("Hobbit " + id3).build());
171+
testUsersRequestObjects.add(UserRequest.builder().id(id4).name("Samwise " + id4).build());
182172

183173
UpdateUsersRequest updateUsersRequest =
184174
UpdateUsersRequest.builder()

src/test/java/io/getstream/ChatTestBase.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,7 @@ protected List<String> createTestUsers(int n) throws Exception {
2222
for (int i = 0; i < n; i++) {
2323
String id = "tu-" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
2424
ids.add(id);
25-
users.put(
26-
id,
27-
UserRequest.builder()
28-
.id(id)
29-
.name("Test User " + id.substring(0, 8))
30-
.role("user")
31-
.build());
25+
users.put(id, UserRequest.builder().id(id).name("Test User " + id).role("user").build());
3226
}
3327
client.updateUsers(UpdateUsersRequest.builder().users(users).build()).execute();
3428
return ids;

src/test/java/io/getstream/ChatUserIntegrationTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ void testUpsertUsers() throws Exception {
3030
Map<String, UserRequest> users = new HashMap<>();
3131
for (int i = 0; i < 2; i++) {
3232
String id = "tu-" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
33-
users.put(id, UserRequest.builder().id(id).name("Test User " + i).role("user").build());
33+
users.put(id, UserRequest.builder().id(id).name("Test User " + id).role("user").build());
3434
}
3535

3636
var resp = client.updateUsers(UpdateUsersRequest.builder().users(users).build()).execute();
@@ -658,7 +658,7 @@ void testUserCustomData() throws Exception {
658658
userId,
659659
UserRequest.builder()
660660
.id(userId)
661-
.name("Custom Data User")
661+
.name("Custom Data User " + userId)
662662
.role("user")
663663
.custom(customData)
664664
.build());

src/test/java/io/getstream/FeedIntegrationTests.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,18 @@ private static void setupEnvironment() throws Exception {
7070
Map<String, UserRequest> usersMap = new HashMap<>();
7171
usersMap.put(
7272
testUserId,
73-
UserRequest.builder().id(testUserId).name("Test User 1").role("user").build());
73+
UserRequest.builder()
74+
.id(testUserId)
75+
.name("Test User " + testUserId)
76+
.role("user")
77+
.build());
7478
usersMap.put(
7579
testUserId2,
76-
UserRequest.builder().id(testUserId2).name("Test User 2").role("user").build());
80+
UserRequest.builder()
81+
.id(testUserId2)
82+
.name("Test User " + testUserId2)
83+
.role("user")
84+
.build());
7785

7886
UpdateUsersRequest updateUsersRequest = UpdateUsersRequest.builder().users(usersMap).build();
7987

src/test/java/io/getstream/ModerationTest.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,22 @@ static void setup() throws Exception {
3737

3838
Map<String, UserRequest> usersMap = new HashMap<>();
3939
usersMap.put(
40-
testUserId, UserRequest.builder().id(testUserId).name("Test User 1").role("user").build());
40+
testUserId,
41+
UserRequest.builder().id(testUserId).name("Test User " + testUserId).role("user").build());
4142
usersMap.put(
4243
testUserId2,
43-
UserRequest.builder().id(testUserId2).name("Test User 2").role("user").build());
44+
UserRequest.builder()
45+
.id(testUserId2)
46+
.name("Test User " + testUserId2)
47+
.role("user")
48+
.build());
4449
usersMap.put(
4550
testModeratorId,
46-
UserRequest.builder().id(testModeratorId).name("Moderator").role("admin").build());
51+
UserRequest.builder()
52+
.id(testModeratorId)
53+
.name("Moderator " + testModeratorId)
54+
.role("admin")
55+
.build());
4756

4857
UpdateUsersRequest updateUsersRequest = UpdateUsersRequest.builder().users(usersMap).build();
4958
client.updateUsers(updateUsersRequest).execute();

src/test/java/io/getstream/StreamHTTPClientTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import static org.junit.jupiter.api.Assertions.*;
44

5+
import com.fasterxml.jackson.core.JsonProcessingException;
56
import com.fasterxml.jackson.databind.ObjectMapper;
67
import io.getstream.models.MessageResponse;
8+
import io.getstream.models.UpdateAppRequest;
79
import io.getstream.services.framework.StreamHTTPClient;
810
import java.util.Date;
911
import org.junit.jupiter.api.BeforeAll;
@@ -63,6 +65,33 @@ void testUnixNanosecondTimestampParsing() throws Exception {
6365
expectedDate.getTime(), message.getUpdatedAt().getTime(), message.getUpdatedAt()));
6466
}
6567

68+
@Test
69+
void testNullFieldsOmittedFromSerialization() throws JsonProcessingException {
70+
// Only set one field, leave everything else null
71+
UpdateAppRequest request = UpdateAppRequest.builder().enforceUniqueUsernames("no").build();
72+
73+
String json = objectMapper.writeValueAsString(request);
74+
75+
// The set field must be present
76+
assertTrue(json.contains("\"enforce_unique_usernames\":\"no\""));
77+
// Null fields must be omitted, not serialized as null
78+
assertFalse(json.contains("null"), "Null fields should be omitted, got: " + json);
79+
assertFalse(json.contains("webhook_url"));
80+
assertFalse(json.contains("multi_tenant_enabled"));
81+
}
82+
83+
@Test
84+
void testCollectionFieldsSerializedWhenSet() throws JsonProcessingException {
85+
// An explicitly set empty list should still be serialized
86+
UpdateAppRequest request = UpdateAppRequest.builder().grants(new java.util.HashMap<>()).build();
87+
88+
String json = objectMapper.writeValueAsString(request);
89+
90+
assertTrue(
91+
json.contains("\"grants\":{}"),
92+
"Empty collections should be serialized when explicitly set, got: " + json);
93+
}
94+
6695
@Test
6796
void testRFC3339TimestampParsing() throws Exception {
6897
// Create a JSON response with RFC 3339 formatted timestamp

src/test/java/io/getstream/VideoIntegrationTest.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -514,8 +514,9 @@ void testUserBlocking() throws Exception {
514514
String bobId = "vid-bob-blk-" + RandomStringUtils.randomAlphabetic(8).toLowerCase();
515515

516516
Map<String, UserRequest> usersMap = new HashMap<>();
517-
usersMap.put(aliceId, UserRequest.builder().id(aliceId).name("Alice Blocker").build());
518-
usersMap.put(bobId, UserRequest.builder().id(bobId).name("Bob Blocked").build());
517+
usersMap.put(
518+
aliceId, UserRequest.builder().id(aliceId).name("Alice Blocker " + aliceId).build());
519+
usersMap.put(bobId, UserRequest.builder().id(bobId).name("Bob Blocked " + bobId).build());
519520
client.updateUsers(UpdateUsersRequest.builder().users(usersMap).build()).execute();
520521

521522
// Block bob from alice's perspective (app-level user block, not call-level)
@@ -699,8 +700,8 @@ void testDeactivateUser() throws Exception {
699700
String bobId = "vid-bob-" + RandomStringUtils.randomAlphabetic(8).toLowerCase();
700701

701702
Map<String, UserRequest> usersMap = new HashMap<>();
702-
usersMap.put(aliceId, UserRequest.builder().id(aliceId).name("Alice Video").build());
703-
usersMap.put(bobId, UserRequest.builder().id(bobId).name("Bob Video").build());
703+
usersMap.put(aliceId, UserRequest.builder().id(aliceId).name("Alice Video " + aliceId).build());
704+
usersMap.put(bobId, UserRequest.builder().id(bobId).name("Bob Video " + bobId).build());
704705
client.updateUsers(UpdateUsersRequest.builder().users(usersMap).build()).execute();
705706

706707
// Deactivate alice
@@ -917,7 +918,11 @@ void testTeams() throws Exception {
917918
Map<String, UserRequest> usersMap = new HashMap<>();
918919
usersMap.put(
919920
userId,
920-
UserRequest.builder().id(userId).name("Teams User").teams(List.of("red", "blue")).build());
921+
UserRequest.builder()
922+
.id(userId)
923+
.name("Teams User " + userId)
924+
.teams(List.of("red", "blue"))
925+
.build());
921926
client.updateUsers(UpdateUsersRequest.builder().users(usersMap).build()).execute();
922927

923928
// Create call with team="blue"

0 commit comments

Comments
 (0)