Skip to content

Commit 7ac15ea

Browse files
committed
fix: improve serialize-nulls in additional properties
This commit improves the behavior of the DynamicModelTypeAdapterFactory's serialization function so that we support the serialization of null values found within additional properties in a more accurate way. 1. A null-valued additional property will be serialized as null in the JSON. 2. A null-valued normal property that is set as an additional property will NOT be serialized as a null. 3. The above "serialize nulls" behavior also applies to additional property values which are model instances.
1 parent 372f4d9 commit 7ac15ea

File tree

2 files changed

+67
-5
lines changed

2 files changed

+67
-5
lines changed

src/main/java/com/ibm/cloud/sdk/core/util/DynamicModelTypeAdapterFactory.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.LinkedHashMap;
2525
import java.util.List;
2626
import java.util.Map;
27+
import java.util.Set;
2728
import java.util.logging.Logger;
2829

2930
import com.google.gson.Gson;
@@ -290,12 +291,14 @@ public static class Adapter<T> extends TypeAdapter<T> {
290291
private Map<String, BoundField> boundFields;
291292
private Gson gson;
292293
private TypeAdapter<?> mapValueObjectTypeAdapter;
294+
private Set<String> boundFieldNames;
293295

294296
Adapter(Gson gson, Constructor<?> ctor, Map<String, BoundField> boundFields) {
295297
this.gson = gson;
296298
this.ctor = ctor;
297299
this.boundFields = boundFields;
298300
this.mapValueObjectTypeAdapter = new MapValueObjectTypeAdapter(gson);
301+
this.boundFieldNames = boundFields.keySet();
299302
}
300303

301304
/*
@@ -336,12 +339,27 @@ public void write(JsonWriter out, T value) throws IOException {
336339
// Next, serialize each of the map entries.
337340
// When serializing the map entries (i.e. additional/dynamic properties) we want
338341
// to explicitly serialize null values regardless of the global Gson "serialize nulls" setting.
342+
// In order to ensure that the "serialize nulls" behavior is not inadvertently pushed down into
343+
// recursive serialization steps (i.e. "mapValue" is a model instance), we'll only set the
344+
// "serialize nulls" option if the dynamic property's value is null AND the dynamic property's name
345+
// is not one of the explicitly-defined properties.
339346
boolean serializeNulls = out.getSerializeNulls();
340-
out.setSerializeNulls(true);
341347
try {
342348
for (String key : ((DynamicModel<?>) value).getPropertyNames()) {
343-
out.name(String.valueOf(key));
344-
mapValueTypeAdapter.write(out, ((DynamicModel<?>) value).get(key));
349+
Object mapValue = ((DynamicModel<?>) value).get(key);
350+
351+
// If this dynamic property is NOT an explicitly-defined property AND the value is null,
352+
// then temporarily enable the "serializeNulls" Gson option.
353+
if (!boundFieldNames.contains(key) && mapValue == null) {
354+
out.setSerializeNulls(true);
355+
}
356+
357+
// Now serialize the key and value.
358+
out.name(key);
359+
mapValueTypeAdapter.write(out, mapValue);
360+
361+
// Restore the original "serializeNulls" option value.
362+
out.setSerializeNulls(serializeNulls);
345363
}
346364
} finally {
347365
out.setSerializeNulls(serializeNulls);

src/test/java/com/ibm/cloud/sdk/core/test/model/DynamicModelSerializationTest.java

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ private void display(String msg) {
6060
}
6161
}
6262

63+
private String trimWhitespace(String s) {
64+
return s.replaceAll("\\s+", "");
65+
}
66+
6367
private <T> void testSerDeser(DynamicModel<?> model, Class<T> clazz) {
6468
String jsonString = serialize(model);
6569

@@ -204,9 +208,49 @@ public void testNoCtor() {
204208

205209
@Test
206210
public void testNullValues() {
207-
ModelAPFoo model = createModelAPFoo();
211+
ModelAPFoo model;
212+
Foo foo;
213+
String json;
214+
215+
// A simple empty model instance (all props null, no additional properties)
216+
// should produce "{}"
217+
model = new ModelAPFoo();
218+
json = serialize(model);
219+
assertEquals(trimWhitespace(json), "{}");
220+
221+
// Explicitly setting a property to null should also produce "{}"
222+
model = new ModelAPFoo();
208223
model.setProp1(null);
209-
testSerDeser(model, ModelAPFoo.class);
224+
json = serialize(model);
225+
assertEquals(trimWhitespace(json), "{}");
226+
227+
// Explicitly setting an additional property to null should
228+
// cause that property to be serialized as a null.
229+
model = new ModelAPFoo();
230+
model.put("foo", null);
231+
json = serialize(model);
232+
assertEquals(trimWhitespace(json), "{\"foo\":null}");
233+
234+
// Setting a normal property as an additional property should
235+
// NOT produce a "null" value in the JSON.
236+
model = new ModelAPFoo();
237+
model.put("prop1", null);
238+
json = serialize(model);
239+
assertEquals(trimWhitespace(json), "{}");
240+
241+
// Setting three additional properties - foo1, foo2, foo3
242+
// foo1 is a model instance with a null property (should NOT be serialized as null)
243+
// foo2 is null and should be serialized as null
244+
// foo3 is a model instance with both properties null (should be serialized as "{}"
245+
model = new ModelAPFoo();
246+
model.setProp1("value1");
247+
foo = new Foo();
248+
foo.setBar(38);
249+
model.put("foo1", foo);
250+
model.put("foo2", null);
251+
model.put("foo3", new Foo());
252+
json = serialize(model);
253+
assertEquals(trimWhitespace(json), "{\"prop1\":\"value1\",\"foo1\":{\"bar\":38},\"foo2\":null,\"foo3\":{}}");
210254
}
211255

212256
@Test

0 commit comments

Comments
 (0)