Skip to content

Commit 8d00706

Browse files
authored
Merge pull request #272 from contentstack/fix/DX-29683-serialize-nulls
feat: add opt-in Builder.setSerializeNulls to send explicit nulls (DX-29683)
2 parents 112118f + f483430 commit 8d00706

4 files changed

Lines changed: 103 additions & 2 deletions

File tree

‎changelog.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## v1.15.0
4+
5+
### Oct 21, 2026
6+
7+
- Feat: Added the opt-in `Contentstack.Builder.setSerializeNulls(boolean)` option. When enabled, `null` values in request bodies are sent to the API instead of being dropped, so a field's value (for example a Number field on a localized entry) can be cleared. Off by default; existing behavior is unchanged.
8+
39
## v1.14.1
410

511
### Aug 26, 2026

‎pom.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<artifactId>cms</artifactId>
88
<packaging>jar</packaging>
99
<name>contentstack-management-java</name>
10-
<version>1.14.1</version>
10+
<version>1.15.0</version>
1111
<description>Contentstack Java Management SDK for Content Management API, Contentstack is a headless CMS with an
1212
API-first approach
1313
</description>

‎src/main/java/com/contentstack/cms/Contentstack.java‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.contentstack.cms.stack.Stack;
3131
import com.contentstack.cms.user.User;
3232
import com.google.gson.Gson;
33+
import com.google.gson.GsonBuilder;
3334
import com.contentstack.cms.core.RetryConfig;
3435
import okhttp3.ConnectionPool;
3536
import okhttp3.OkHttpClient;
@@ -696,6 +697,7 @@ public static class Builder {
696697
private Integer readTimeoutSeconds;
697698
private Integer writeTimeoutSeconds;
698699
private Boolean retry = Util.RETRY_ON_FAILURE;// Default base url for contentstack
700+
private boolean serializeNulls = false;
699701
private RetryConfig retryConfig = RetryConfig.defaultConfig();
700702
/**
701703
* Default ConnectionPool holds up to 5 idle connections which will be
@@ -744,6 +746,22 @@ public Builder setRetry(@NotNull Boolean retry) {
744746
return this;
745747
}
746748

749+
/**
750+
* Controls whether {@code null} values in request bodies are sent to the API.
751+
* <p>
752+
* By default {@code null} values are dropped before the request is sent, so
753+
* {@code "field": null} behaves exactly like leaving the field out. Enable this
754+
* to send {@code null} explicitly, for example to clear a Number field's value
755+
* on an entry. It applies to every request made through this client.
756+
*
757+
* @param serializeNulls {@code true} to send {@code null} values, {@code false} (default) to drop them
758+
* @return the Builder
759+
*/
760+
public Builder setSerializeNulls(boolean serializeNulls) {
761+
this.serializeNulls = serializeNulls;
762+
return this;
763+
}
764+
747765
/**
748766
* Set host for client instance
749767
*
@@ -1015,8 +1033,12 @@ private void validateClient(Contentstack contentstack) {
10151033
// of how the hostname was set (setHost, region resolution, or default).
10161034
String validatedHost = validateHostname(this.hostname);
10171035
String baseUrl = Util.PROTOCOL + "://" + validatedHost + "/" + version + "/";
1036+
GsonBuilder gson = new GsonBuilder();
1037+
if (serializeNulls) {
1038+
gson.serializeNulls();
1039+
}
10181040
this.instance = new Retrofit.Builder().baseUrl(baseUrl)
1019-
.addConverterFactory(GsonConverterFactory.create())
1041+
.addConverterFactory(GsonConverterFactory.create(gson.create()))
10201042
.client(httpClient(contentstack, this.retry)).build();
10211043
contentstack.instance = this.instance;
10221044

‎src/test/java/com/contentstack/cms/ContentstackUnitTest.java‎

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

33
import com.contentstack.cms.core.AuthInterceptor;
44
import com.contentstack.cms.organization.Organization;
5+
import com.contentstack.cms.stack.Entry;
56
import com.contentstack.cms.stack.Stack;
7+
import com.google.gson.JsonObject;
8+
import com.google.gson.JsonParser;
69
import okhttp3.Headers;
710
import okhttp3.OkHttpClient;
811
import okhttp3.Protocol;
@@ -12,7 +15,10 @@
1215
import okhttp3.mockwebserver.MockWebServer;
1316
import okhttp3.mockwebserver.RecordedRequest;
1417
import org.junit.jupiter.api.Assertions;
18+
import okio.Buffer;
19+
import org.json.simple.JSONObject;
1520
import org.junit.jupiter.api.Test;
21+
import retrofit2.Call;
1622
import retrofit2.Response;
1723

1824
import java.io.IOException;
@@ -412,4 +418,71 @@ public void testEarlyAccessHeaderEmpty() {
412418
Assertions.assertEquals(0, client.earlyAccess.length);
413419
}
414420

421+
@Test
422+
void localizeBodyKeepsNullFieldWhenSerializeNullsEnabled() throws IOException {
423+
Contentstack client = new Contentstack.Builder().setSerializeNulls(true).build();
424+
425+
JsonObject entry = sentEntry(entryOf(client).localize(entryBody(fieldsWith("position", null)), "de-de"));
426+
427+
Assertions.assertTrue(entry.has("position"));
428+
Assertions.assertTrue(entry.get("position").isJsonNull());
429+
}
430+
431+
@Test
432+
void localizeBodyDropsNullFieldByDefault() throws IOException {
433+
Contentstack client = new Contentstack.Builder().build();
434+
435+
JsonObject entry = sentEntry(entryOf(client).localize(entryBody(fieldsWith("position", null)), "de-de"));
436+
437+
Assertions.assertFalse(entry.has("position"));
438+
Assertions.assertEquals("example", entry.get("title").getAsString());
439+
}
440+
441+
@Test
442+
void localizeBodyKeepsNullInsideGroupFieldWhenSerializeNullsEnabled() throws IOException {
443+
Contentstack client = new Contentstack.Builder().setSerializeNulls(true).build();
444+
Map<String, Object> group = new HashMap<>();
445+
group.put("label", "kept");
446+
group.put("position", null);
447+
448+
JsonObject entry = sentEntry(entryOf(client).localize(entryBody(fieldsWith("group", group)), "de-de"));
449+
450+
JsonObject sentGroup = entry.getAsJsonObject("group");
451+
Assertions.assertTrue(sentGroup.get("position").isJsonNull());
452+
Assertions.assertEquals("kept", sentGroup.get("label").getAsString());
453+
}
454+
455+
@Test
456+
void updateBodyKeepsNullFieldWhenSerializeNullsEnabled() throws IOException {
457+
Contentstack client = new Contentstack.Builder().setSerializeNulls(true).build();
458+
459+
JsonObject entry = sentEntry(entryOf(client).update(entryBody(fieldsWith("position", null))));
460+
461+
Assertions.assertTrue(entry.has("position"));
462+
Assertions.assertTrue(entry.get("position").isJsonNull());
463+
}
464+
465+
private static Map<String, Object> fieldsWith(String field, Object value) {
466+
Map<String, Object> fields = new HashMap<>();
467+
fields.put("title", "example");
468+
fields.put(field, value);
469+
return fields;
470+
}
471+
472+
private static JSONObject entryBody(Map<String, Object> fields) {
473+
Map<String, Object> body = new HashMap<>();
474+
body.put("entry", fields);
475+
return new JSONObject(body);
476+
}
477+
478+
private static Entry entryOf(Contentstack client) {
479+
return client.stack("api_key", "management_token").contentType("newspage").entry("entry_uid");
480+
}
481+
482+
private static JsonObject sentEntry(Call<ResponseBody> call) throws IOException {
483+
Buffer buffer = new Buffer();
484+
call.request().body().writeTo(buffer);
485+
return JsonParser.parseString(buffer.readUtf8()).getAsJsonObject().getAsJsonObject("entry");
486+
}
487+
415488
}

0 commit comments

Comments
 (0)