Skip to content

Commit

Permalink
"Explicit" nulls within RuntimeTypeAdapterFactory
Browse files Browse the repository at this point in the history
Using custom TypeAdapters, it is possible to force null values within a
particular type to be explicitly serialized, by overriding the
setSerializeNulls setting on the supplied JsonWriter.

However, this behavior fails in the context of a
RuntimeTypeAdapterFactory because a second JsonTreeWriter is used and
settings are not propagated from one to the other.

This change fixes this by:

- propagating the original setSerializeNulls setting to the created
JsonTreeWriter
- temporarily forcing the setSerializeNulls setting to true on the
oriiginal JsonWriter to ensure any explicit nulls are preserved.

Issue #676
  • Loading branch information
lcoote committed Aug 5, 2015
1 parent 77e31ed commit a1d5e9c
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.Streams;
import com.google.gson.internal.bind.JsonTreeWriter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
Expand Down Expand Up @@ -215,6 +217,7 @@ public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
}

@Override public void write(JsonWriter out, R value) throws IOException {
boolean serializeNulls = out.getSerializeNulls();
Class<?> srcType = value.getClass();
String label = subtypeToLabel.get(srcType);
@SuppressWarnings("unchecked") // registration requires that subtype extends T
Expand All @@ -223,7 +226,7 @@ public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ "; did you forget to register a subtype?");
}
JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
JsonObject jsonObject = toJsonObject(delegate, value, serializeNulls);
if (jsonObject.has(typeFieldName)) {
throw new JsonParseException("cannot serialize " + srcType.getName()
+ " because it already defines a field named " + typeFieldName);
Expand All @@ -233,7 +236,24 @@ public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
Streams.write(clone, out);
// preserve any explicit nulls in the object
out.setSerializeNulls(true);
try {
Streams.write(clone, out);
} finally {
out.setSerializeNulls(serializeNulls);
}
}

protected JsonObject toJsonObject(TypeAdapter<R> delegate, R value, boolean serializeNulls) {
try {
JsonTreeWriter jsonWriter = new JsonTreeWriter();
jsonWriter.setSerializeNulls(serializeNulls);
delegate.write(jsonWriter, value);
return jsonWriter.get().getAsJsonObject();
} catch (IOException e) {
throw new JsonIOException(e);
}
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,19 @@

package com.google.gson.typeadapters;

import java.io.IOException;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import junit.framework.TestCase;

public final class RuntimeTypeAdapterFactoryTest extends TestCase {
Expand Down Expand Up @@ -189,4 +198,91 @@ static class BankTransfer extends BillingInstrument {
this.bankAccount = bankAccount;
}
}

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 {
String other;
OuterB(Inner wrapped, String other) {
super(wrapped);
}
}

static class Inner {
String prop;
Inner(String prop) {
this.prop = prop;
}
}

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);
}

}

public void testSerializingToPreserveNullsInEmbeddedObjects() {
TypeAdapterFactory metaWrapperAdapter = RuntimeTypeAdapterFactory.of(Outer.class, "type")
.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));

// verify, null serialization works when wrapped in runtime-type-driven wrapper
// note: setting "other" to null, to ensure default non-null-serializing behavior is preserved
OuterB outer = new OuterB(inner, null);
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);
}

}

0 comments on commit a1d5e9c

Please sign in to comment.