Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## v1.15.0

### Oct 21, 2026

- 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.

## v1.14.1

### Aug 26, 2026
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<artifactId>cms</artifactId>
<packaging>jar</packaging>
<name>contentstack-management-java</name>
<version>1.14.1</version>
<version>1.15.0</version>
<description>Contentstack Java Management SDK for Content Management API, Contentstack is a headless CMS with an
API-first approach
</description>
Expand Down
24 changes: 23 additions & 1 deletion src/main/java/com/contentstack/cms/Contentstack.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import com.contentstack.cms.stack.Stack;
import com.contentstack.cms.user.User;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.contentstack.cms.core.RetryConfig;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
Expand Down Expand Up @@ -696,6 +697,7 @@ public static class Builder {
private Integer readTimeoutSeconds;
private Integer writeTimeoutSeconds;
private Boolean retry = Util.RETRY_ON_FAILURE;// Default base url for contentstack
private boolean serializeNulls = false;
private RetryConfig retryConfig = RetryConfig.defaultConfig();
/**
* Default ConnectionPool holds up to 5 idle connections which will be
Expand Down Expand Up @@ -744,6 +746,22 @@ public Builder setRetry(@NotNull Boolean retry) {
return this;
}

/**
* Controls whether {@code null} values in request bodies are sent to the API.
* <p>
* By default {@code null} values are dropped before the request is sent, so
* {@code "field": null} behaves exactly like leaving the field out. Enable this
* to send {@code null} explicitly, for example to clear a Number field's value
* on an entry. It applies to every request made through this client.
*
* @param serializeNulls {@code true} to send {@code null} values, {@code false} (default) to drop them
* @return the Builder
*/
public Builder setSerializeNulls(boolean serializeNulls) {
this.serializeNulls = serializeNulls;
return this;
}

/**
* Set host for client instance
*
Expand Down Expand Up @@ -1015,8 +1033,12 @@ private void validateClient(Contentstack contentstack) {
// of how the hostname was set (setHost, region resolution, or default).
String validatedHost = validateHostname(this.hostname);
String baseUrl = Util.PROTOCOL + "://" + validatedHost + "/" + version + "/";
GsonBuilder gson = new GsonBuilder();
if (serializeNulls) {
gson.serializeNulls();
}
this.instance = new Retrofit.Builder().baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson.create()))
.client(httpClient(contentstack, this.retry)).build();
contentstack.instance = this.instance;

Expand Down
73 changes: 73 additions & 0 deletions src/test/java/com/contentstack/cms/ContentstackUnitTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import com.contentstack.cms.core.AuthInterceptor;
import com.contentstack.cms.organization.Organization;
import com.contentstack.cms.stack.Entry;
import com.contentstack.cms.stack.Stack;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
Expand All @@ -12,7 +15,10 @@
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.Assertions;
import okio.Buffer;
import org.json.simple.JSONObject;
import org.junit.jupiter.api.Test;
import retrofit2.Call;
import retrofit2.Response;

import java.io.IOException;
Expand Down Expand Up @@ -412,4 +418,71 @@ public void testEarlyAccessHeaderEmpty() {
Assertions.assertEquals(0, client.earlyAccess.length);
}

@Test
void localizeBodyKeepsNullFieldWhenSerializeNullsEnabled() throws IOException {
Contentstack client = new Contentstack.Builder().setSerializeNulls(true).build();

JsonObject entry = sentEntry(entryOf(client).localize(entryBody(fieldsWith("position", null)), "de-de"));

Assertions.assertTrue(entry.has("position"));
Assertions.assertTrue(entry.get("position").isJsonNull());
}

@Test
void localizeBodyDropsNullFieldByDefault() throws IOException {
Contentstack client = new Contentstack.Builder().build();

JsonObject entry = sentEntry(entryOf(client).localize(entryBody(fieldsWith("position", null)), "de-de"));

Assertions.assertFalse(entry.has("position"));
Assertions.assertEquals("example", entry.get("title").getAsString());
}

@Test
void localizeBodyKeepsNullInsideGroupFieldWhenSerializeNullsEnabled() throws IOException {
Contentstack client = new Contentstack.Builder().setSerializeNulls(true).build();
Map<String, Object> group = new HashMap<>();
group.put("label", "kept");
group.put("position", null);

JsonObject entry = sentEntry(entryOf(client).localize(entryBody(fieldsWith("group", group)), "de-de"));

JsonObject sentGroup = entry.getAsJsonObject("group");
Assertions.assertTrue(sentGroup.get("position").isJsonNull());
Assertions.assertEquals("kept", sentGroup.get("label").getAsString());
}

@Test
void updateBodyKeepsNullFieldWhenSerializeNullsEnabled() throws IOException {
Contentstack client = new Contentstack.Builder().setSerializeNulls(true).build();

JsonObject entry = sentEntry(entryOf(client).update(entryBody(fieldsWith("position", null))));

Assertions.assertTrue(entry.has("position"));
Assertions.assertTrue(entry.get("position").isJsonNull());
}

private static Map<String, Object> fieldsWith(String field, Object value) {
Map<String, Object> fields = new HashMap<>();
fields.put("title", "example");
fields.put(field, value);
return fields;
}

private static JSONObject entryBody(Map<String, Object> fields) {
Map<String, Object> body = new HashMap<>();
body.put("entry", fields);
return new JSONObject(body);
}

private static Entry entryOf(Contentstack client) {
return client.stack("api_key", "management_token").contentType("newspage").entry("entry_uid");
}

private static JsonObject sentEntry(Call<ResponseBody> call) throws IOException {
Buffer buffer = new Buffer();
call.request().body().writeTo(buffer);
return JsonParser.parseString(buffer.readUtf8()).getAsJsonObject().getAsJsonObject("entry");
}

}
Loading