google / google/gson

`RuntimeTypeAdapterFactory` overrides explicit control over `serializeNulls` behavior of contained objects

Open
#676 0 comments 2 reactions 0 assignees View on GitHub
bug
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.