From a1d5e9c6fd8ec0a5e0acfc038732f597ac744714 Mon Sep 17 00:00:00 2001 From: Lachlan Coote Date: Tue, 4 Aug 2015 17:38:00 -0700 Subject: [PATCH] "Explicit" nulls within RuntimeTypeAdapterFactory 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 --- .../RuntimeTypeAdapterFactory.java | 24 ++++- .../RuntimeTypeAdapterFactoryTest.java | 96 +++++++++++++++++++ 2 files changed, 118 insertions(+), 2 deletions(-) diff --git a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java index 1111ebc642..891f579f1b 100644 --- a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java +++ b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java @@ -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; @@ -215,6 +217,7 @@ public TypeAdapter create(Gson gson, TypeToken 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 @@ -223,7 +226,7 @@ public TypeAdapter create(Gson gson, TypeToken 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); @@ -233,7 +236,24 @@ public TypeAdapter create(Gson gson, TypeToken type) { for (Map.Entry 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 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); + } } }; } diff --git a/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java b/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java index a9f8ebbbe7..904e2a62d5 100644 --- a/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java +++ b/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java @@ -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 { @@ -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 { + + @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); + } + }