diff --git a/gson/src/main/java/com/google/gson/internal/Excluder.java b/gson/src/main/java/com/google/gson/internal/Excluder.java index 14ecd60fbe..5827f6075d 100644 --- a/gson/src/main/java/com/google/gson/internal/Excluder.java +++ b/gson/src/main/java/com/google/gson/internal/Excluder.java @@ -24,6 +24,7 @@ import com.google.gson.annotations.Expose; import com.google.gson.annotations.Since; import com.google.gson.annotations.Until; +import com.google.gson.internal.bind.SerializationDelegatingTypeAdapter; import com.google.gson.internal.reflect.ReflectionHelper; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; @@ -118,7 +119,7 @@ public TypeAdapter create(Gson gson, TypeToken type) { return null; } - return new TypeAdapter() { + return new SerializationDelegatingTypeAdapter() { /** * The delegate is lazily created because it may not be needed, and creating it may fail. * Field has to be {@code volatile} because {@link Gson} guarantees to be thread-safe. @@ -152,6 +153,13 @@ private TypeAdapter delegate() { } return d; } + + @Override + public TypeAdapter getSerializationDelegate() { + // When skipSerialize is true this adapter handles serialization itself (writes null), + // so there is no delegation. Otherwise it delegates to the next adapter in the chain. + return skipSerialize ? this : delegate(); + } }; } diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java index f64dbc6562..86e5b888a0 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java @@ -55,10 +55,7 @@ public void write(JsonWriter out, T value) throws IOException { @SuppressWarnings("unchecked") TypeAdapter runtimeTypeAdapter = (TypeAdapter) context.getAdapter(TypeToken.get(runtimeType)); - // For backward compatibility only check ReflectiveTypeAdapterFactory.Adapter here but not any - // other wrapping adapters, see - // https://github.com/google/gson/pull/1787#issuecomment-1222175189 - if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter)) { + if (!isReflective(runtimeTypeAdapter)) { // The user registered a type adapter for the runtime type, so we will use that chosen = runtimeTypeAdapter; } else if (!isReflective(delegate)) { diff --git a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java index 82fe45ff6a..84906f3b6c 100644 --- a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java +++ b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java @@ -18,6 +18,8 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; @@ -151,14 +153,13 @@ public void testJsonDeserializer_JsonSerializerDelegate() { } /** - * When a {@link JsonDeserializer} is registered for Subclass, and a custom {@link JsonSerializer} - * is registered for Base, then Gson should prefer the reflective adapter for Subclass for - * backward compatibility (see https://github.com/google/gson/pull/1787#issuecomment-1222175189) - * even though normally TypeAdapterRuntimeTypeWrapper should prefer the custom serializer for - * Base. + * When a {@link JsonDeserializer} is registered for Subclass (which only affects + * deserialization), and a custom {@link JsonSerializer} is registered for Base, then Gson should + * prefer the custom serializer for Base because the Subclass deserializer has no effect on + * serialization. */ @Test - public void testJsonDeserializer_SubclassBackwardCompatibility() { + public void testJsonDeserializer_SubclassWithBaseSerializer() { Gson gson = new GsonBuilder() .registerTypeAdapter( @@ -173,7 +174,47 @@ public void testJsonDeserializer_SubclassBackwardCompatibility() { .create(); String json = gson.toJson(new Container()); - assertThat(json).isEqualTo("{\"b\":{\"f\":\"test\"}}"); + assertThat(json).isEqualTo("{\"b\":\"base\"}"); + } + + /** + * Regression test for https://github.com/google/gson/issues/2190: a deserialization-only + * ExclusionStrategy that excludes the runtime type must not interfere with serialization. The + * custom TypeAdapter registered for the declared type should still be used. + */ + @Test + public void testDeserializationExclusionStrategy_CustomBaseAdapter() { + Gson gson = + new GsonBuilder() + .addDeserializationExclusionStrategy( + new ExclusionStrategy() { + @Override + public boolean shouldSkipField(FieldAttributes f) { + return false; + } + + @Override + public boolean shouldSkipClass(Class clazz) { + return clazz == Subclass.class; + } + }) + .registerTypeAdapter( + Base.class, + new TypeAdapter() { + @Override + public Base read(JsonReader in) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void write(JsonWriter out, Base value) throws IOException { + out.value("custom-adapter"); + } + }) + .create(); + + String json = gson.toJson(new Container()); + assertThat(json).isEqualTo("{\"b\":\"custom-adapter\"}"); } private static class CyclicBase {