Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix Issue #676 - "explicit" nulls within RuntimeTypeAdapterFactory #677

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}

}