-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Description
By default serializeNulls behavior is off for GSON which is useful for building compact JSON. However, for specific types of objects its necessary to switch this on to make a distinction between (a) properties for which no explicit value has been assigned vs (b) properties for which a null value has been explicitly assigned.
One way this is achieved is to use a TypeAdapter implementation to temporarily override the setting of this behavior on the JsonWriter.
E.g.
static class TypeAdapterForSomeObject extends TypeAdapter<SomeObject> {
@Override
public void write(JsonWriter out, SomeObject value) throws IOException {
boolean oldSerializeNulls = out.getSerializeNulls();
try {
out.setSerializeNulls(true);
// serialization impl...
} finally {
out.setSerializeNulls(oldSerializeNulls);
}
}
// ...
}
However, when such a type is part of another class which is serialized within another wrapper object which relies on a RuntimeTypeAdapterFactory, these explicit null override behavior can be lost. That is because, the implementation of TypeAdapter.write initially serializes the outer value using a temporary JsonTreeWriter into a JsonElement. This temporary writer invokes the custom TypeAdapter and thus accurately collects the explicit null values. However, RuntimeTypeAdapterFactory then serializes the JsonElement using the externally provided JsonWriter using its own serializeNulls configuration and therefore the explicit null values in the element are discarded.
A full test is included below:
public class Tests {
// define value requiring explicit nulls
static class Inner {
String prop;
Inner(String prop) {
this.prop = prop;
}
}
// define adapter to switch on explicit null
static class TypeAdapterForInner extends TypeAdapter<Inner> {
@Override
public void write(JsonWriter out, Inner value) throws IOException {
boolean oldSerializeNulls = out.getSerializeNulls();
try {
out.setSerializeNulls(true);
out.beginObject().name("prop").value(value.prop).endObject();
} finally {
out.setSerializeNulls(oldSerializeNulls);
}
}
@Override
public Inner read(JsonReader in) throws IOException {
in.beginObject();
in.nextName();
String value = null;
if (in.peek() == JsonToken.STRING) {
value = in.nextString();
} else {
in.nextNull();
}
in.endObject();
return new Inner(value);
}
}
// define wrapper type hierarchy for use by RuntimeTypeAdapterFactory
static abstract class Outer {
Inner wrapped;
Outer(Inner wrapped) {
this.wrapped = wrapped;
}
}
static class OuterA extends Outer {
OuterA(Inner wrapped) {
super(wrapped);
}
}
static class OuterB extends Outer {
OuterB(Inner wrapped) {
super(wrapped);
}
}
@Test
public void testSerializingToPreserveNullsInEmbeddedObjects() {
TypeAdapterFactory metaWrapperAdapter = RuntimeTypeAdapterFactory.of(Outer.class, "kind")
.registerSubtype(OuterA.class, "a")
.registerSubtype(OuterB.class, "b");
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(metaWrapperAdapter)
.registerTypeAdapter(Inner.class, new TypeAdapterForInner().nullSafe())
.create();
// verify, null serialization works for unwrapped, inner value
Inner inner = new Inner(null);
assertJsonEquivalent(
"{ prop: null }",
gson.toJson(inner));
OuterB outer = new OuterB(inner);
// will fail; actual = "{ type: 'b', wrapped: {} }"
assertJsonEquivalent(
"{ type: 'b', wrapped: { prop: null } }",
gson.toJson(outer, Outer.class));
}
private static void assertJsonEquivalent(String expected, String actual) {
JsonElement expectedElem = new JsonParser().parse(expected);
JsonElement actualElem = new JsonParser().parse(actual);
assertEquals(expectedElem, actualElem);
}
}