`RuntimeTypeAdapterFactory` overrides explicit control over `serializeNulls` behavior of contained objects
- Dominant language
- Java
- Stars
- 24.2k
- Forks
- 4.5k
- Avg merge
- 6d 4h
- Merged PRs (30d)
- 12
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 {
@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 {
@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);
}
}
```
Contributor guide
Assessment
This issue has not been assessed yet.