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 6c6e82e2ad..74255742b5 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 @@ -16,6 +16,7 @@ package com.google.gson.internal.bind; import com.google.gson.Gson; +import com.google.gson.JsonIOException; import com.google.gson.TypeAdapter; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; @@ -56,22 +57,35 @@ public void write(JsonWriter out, T value) throws IOException { @SuppressWarnings("ReferenceEquality") boolean isDifferentType = runtimeType != type; if (isDifferentType) { - @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)) { - // The user registered a type adapter for the runtime type, so we will use that - chosen = runtimeTypeAdapter; - } else if (!isReflective(delegate)) { - // The user registered a type adapter for Base class, so we prefer it over the - // reflective type adapter for the runtime type - chosen = delegate; - } else { - // Use the type adapter for runtime type - chosen = runtimeTypeAdapter; + TypeAdapter runtimeTypeAdapter = null; + try { + @SuppressWarnings("unchecked") + TypeAdapter adapter = (TypeAdapter) context.getAdapter(TypeToken.get(runtimeType)); + runtimeTypeAdapter = adapter; + } catch (JsonIOException e) { + // Building an adapter for the runtime type can fail (for example when reflective + // access to a JDK-internal implementation class is denied). Only propagate that + // failure if we would actually have used the runtime-type adapter; otherwise keep + // the non-reflective declared-type adapter. See https://github.com/google/gson/issues/3122 + if (isReflective(delegate)) { + throw e; + } + } + if (runtimeTypeAdapter != null) { + // 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)) { + // The user registered a type adapter for the runtime type, so we will use that + chosen = runtimeTypeAdapter; + } else if (!isReflective(delegate)) { + // The user registered a type adapter for Base class, so we prefer it over the + // reflective type adapter for the runtime type + chosen = delegate; + } else { + // Use the type adapter for runtime type + chosen = runtimeTypeAdapter; + } } } chosen.write(out, value); 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..7d696c8d9d 100644 --- a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java +++ b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java @@ -17,19 +17,28 @@ package com.google.gson.functional; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializer; +import com.google.gson.ReflectionAccessFilter.FilterResult; import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.IOException; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.List; +import org.junit.AssumptionViolatedException; import org.junit.Test; public class TypeAdapterRuntimeTypeWrapperTest { @@ -69,6 +78,21 @@ public void testJsonSerializer() { assertThat(json).isEqualTo("{\"b\":\"serializer\"}"); } + @Test + public void testNonReflectiveDelegateWhenRuntimeReflectionIsBlocked() { + Gson gson = + new GsonBuilder() + .addReflectionAccessFilter( + type -> type == Subclass.class ? FilterResult.BLOCK_ALL : FilterResult.INDECISIVE) + .registerTypeAdapter( + Base.class, + (JsonSerializer) (src, typeOfSrc, context) -> new JsonPrimitive("serializer")) + .create(); + + assertThrows(JsonIOException.class, () -> gson.getAdapter(Subclass.class)); + assertThat(gson.toJson(new Container())).isEqualTo("{\"b\":\"serializer\"}"); + } + /** * When only {@link JsonDeserializer} is registered for Base, then on serialization should prefer * reflective adapter for Subclass since Base would use reflective adapter as delegate. @@ -202,4 +226,62 @@ public void testGsonFutureAdapter() { String json = new Gson().toJson(b); assertThat(json).isEqualTo("{\"f\":{\"i\":2}}"); } + + /** + * A trivial adapter for {@link WildcardType}. It never uses reflection. + * + *

Regression test for https://github.com/google/gson/issues/3122: when serializing a value as + * an array/collection element, {@code TypeAdapterRuntimeTypeWrapper} must not fail while eagerly + * resolving a reflective adapter for an inaccessible runtime implementation class if the + * declared-type adapter would be preferred anyway. + */ + private static final TypeAdapter WILDCARD_ADAPTER = + new TypeAdapter() { + @Override + public void write(JsonWriter out, WildcardType value) throws IOException { + out.value(value == null ? null : value.getTypeName()); + } + + @Override + public WildcardType read(JsonReader in) throws IOException { + throw new UnsupportedOperationException(); + } + }; + + private static WildcardType sampleWildcard() throws Exception { + class Holder { + @SuppressWarnings("unused") + List field; + } + + ParameterizedType listOfWildcard = + (ParameterizedType) Holder.class.getDeclaredField("field").getGenericType(); + return (WildcardType) listOfWildcard.getActualTypeArguments()[0]; + } + + @Test + public void testNonReflectiveInterfaceAdapter_PrefersDelegateWhenRuntimeReflectiveFails() + throws Exception { + WildcardType wildcard = sampleWildcard(); + // Runtime class is typically sun.reflect.generics.reflectiveObjects.WildcardTypeImpl, which is + // not reflectively accessible under the module system. + try { + var unused = new Gson().getAdapter(TypeToken.get(wildcard.getClass())); + // If reflective access happens to succeed on this JDK, the bug cannot be reproduced here. + throw new AssumptionViolatedException("Runtime wildcard type is reflectively accessible"); + } catch (JsonIOException expected) { + // Continue with the regression assertions. + } + + Gson gson = + new GsonBuilder().registerTypeAdapter(WildcardType.class, WILDCARD_ADAPTER).create(); + + String expected = "\"" + wildcard.getTypeName() + "\""; + assertThat(gson.toJson(wildcard, WildcardType.class)).isEqualTo(expected); + assertThat(gson.toJson(new WildcardType[] {wildcard}, WildcardType[].class)) + .isEqualTo("[" + expected + "]"); + assertThat( + gson.toJson(Arrays.asList(wildcard), new TypeToken>() {}.getType())) + .isEqualTo("[" + expected + "]"); + } }