From a4d4fdcbe893b605e8548b633381e4a2c5808d6a Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Wed, 21 Oct 2026 11:36:42 +0530 Subject: [PATCH 1/3] feat: add opt-in Builder.setSerializeNulls to send explicit nulls (DX-29683) The SDK serializes request bodies with a default Gson converter, which drops null values, so "field": null never reached the CMA and a field such as a Number on a localized entry could not be cleared. Add Contentstack.Builder.setSerializeNulls(boolean), off by default so existing behavior is unchanged. When enabled, explicit nulls are sent for every request made through the client. Bump version to 1.15.0 and add a changelog entry. Co-Authored-By: Claude Sonnet 5.5 --- changelog.md | 6 ++ pom.xml | 2 +- .../com/contentstack/cms/Contentstack.java | 24 +++++- .../cms/ContentstackUnitTest.java | 73 +++++++++++++++++++ 4 files changed, 103 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 118dbb93..8543ea85 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/pom.xml b/pom.xml index 056f6622..da74f14a 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ cms jar contentstack-management-java - 1.14.1 + 1.15.0 Contentstack Java Management SDK for Content Management API, Contentstack is a headless CMS with an API-first approach diff --git a/src/main/java/com/contentstack/cms/Contentstack.java b/src/main/java/com/contentstack/cms/Contentstack.java index a4a75d22..9773a088 100644 --- a/src/main/java/com/contentstack/cms/Contentstack.java +++ b/src/main/java/com/contentstack/cms/Contentstack.java @@ -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; @@ -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 @@ -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. + *

+ * 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 * @@ -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; diff --git a/src/test/java/com/contentstack/cms/ContentstackUnitTest.java b/src/test/java/com/contentstack/cms/ContentstackUnitTest.java index 1da49474..881913ee 100644 --- a/src/test/java/com/contentstack/cms/ContentstackUnitTest.java +++ b/src/test/java/com/contentstack/cms/ContentstackUnitTest.java @@ -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; @@ -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; @@ -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 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 fieldsWith(String field, Object value) { + Map fields = new HashMap<>(); + fields.put("title", "example"); + fields.put(field, value); + return fields; + } + + private static JSONObject entryBody(Map fields) { + Map 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 call) throws IOException { + Buffer buffer = new Buffer(); + call.request().body().writeTo(buffer); + return JsonParser.parseString(buffer.readUtf8()).getAsJsonObject().getAsJsonObject("entry"); + } + } From e52b5d24f97cda7d860e85df4a12996cfe2178de Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Mon, 26 Oct 2026 16:25:43 +0530 Subject: [PATCH 2/3] Fix: use approved placeholder in tests Generated username and API-key fallback in tests; remove deepcode ignore comments. Co-Authored-By: Claude Sonnet 5.5 --- src/test/java/com/contentstack/cms/TestClient.java | 5 +++-- .../java/com/contentstack/cms/models/LoginDetailTest.java | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/contentstack/cms/TestClient.java b/src/test/java/com/contentstack/cms/TestClient.java index e84a9d7e..3995228f 100644 --- a/src/test/java/com/contentstack/cms/TestClient.java +++ b/src/test/java/com/contentstack/cms/TestClient.java @@ -1,5 +1,6 @@ package com.contentstack.cms; +import java.util.UUID; import com.contentstack.cms.core.Util; import com.contentstack.cms.stack.Stack; import io.github.cdimascio.dotenv.Dotenv; @@ -17,8 +18,8 @@ public class TestClient { public final static String USER_ID = (env.get("userId") != null) ? env.get("userId") : "c11e668e0295477f"; public final static String OWNERSHIP = (env.get("ownershipToken") != null) ? env.get("ownershipToken") : "ownershipTokenId"; - // file deepcode ignore NonCryptoHardcodedSecret/test: - public final static String API_KEY = (env.get("apiKey") != null) ? env.get("apiKey") : "apiKey99999999"; + public final static String API_KEY = (env.get("apiKey") != null) ? env.get("apiKey") + : "blt" + UUID.randomUUID().toString().replace("-", "").substring(0, 16); public final static String MANAGEMENT_TOKEN = (env.get("managementToken") != null) ? env.get("managementToken") : "managementToken99999999"; diff --git a/src/test/java/com/contentstack/cms/models/LoginDetailTest.java b/src/test/java/com/contentstack/cms/models/LoginDetailTest.java index 5e070556..73769a7d 100644 --- a/src/test/java/com/contentstack/cms/models/LoginDetailTest.java +++ b/src/test/java/com/contentstack/cms/models/LoginDetailTest.java @@ -1,5 +1,6 @@ package com.contentstack.cms.models; +import java.util.UUID; import com.contentstack.cms.core.AuthInterceptor; import okhttp3.Interceptor; import okhttp3.internal.http.RealInterceptorChain; @@ -102,9 +103,9 @@ void getterSetterUserModelLastName() { @Test void getterSetterUserModelUsername() { UserModel userModel = new UserModel(); - // deepcode ignore NoHardcodedCredentials/test: - userModel.setUsername("***REMOVED***"); - Assertions.assertEquals("***REMOVED***", + String username = UUID.randomUUID().toString(); + userModel.setUsername(username); + Assertions.assertEquals(username, userModel.getUsername()); } From 9ada43dc8ffd4fe8e4abc78180fd5a097635df51 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Thu, 29 Oct 2026 12:03:37 +0530 Subject: [PATCH 3/3] docs: set 1.15.0 release date and note Snyk test fixes in changelog Co-Authored-By: Claude Sonnet 5.5 --- changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 8543ea85..b101af0d 100644 --- a/changelog.md +++ b/changelog.md @@ -2,9 +2,10 @@ ## v1.15.0 -### Oct 21, 2026 +### Nov 02, 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. +- Tests: Replaced hardcoded usernames and API keys in `LoginDetailTest` and `TestClient` with generated values to resolve Snyk Code findings (DX-22133, DX-22134). No change to the published artifact. ## v1.14.1