From 544c631de11036dadda8e61e4b6a9c467ba99bc0 Mon Sep 17 00:00:00 2001 From: Jaideep Pyne Date: Wed, 16 Sep 2026 05:14:34 +0000 Subject: [PATCH 1/3] Fix TypeAdapterRuntimeTypeWrapper eager runtime adapter lookup Do not let JsonIOException from building an unused reflective runtime-type adapter escape when a non-reflective declared-type adapter would be preferred. Fixes #3122. --- .../bind/TypeAdapterRuntimeTypeWrapper.java | 49 ++++++++----- .../TypeAdapterRuntimeTypeWrapperTest.java | 68 ++++++++++++++++++- 2 files changed, 99 insertions(+), 18 deletions(-) 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..7fb54d102c 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,36 @@ 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); @@ -119,4 +134,4 @@ private static Type getRuntimeTypeIfMoreSpecific(Type type, Object value) { } return type; } -} +} \ No newline at end of file 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..dd0e12ccc2 100644 --- a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java +++ b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java @@ -23,13 +23,19 @@ 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.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.Test; public class TypeAdapterRuntimeTypeWrapperTest { @@ -202,4 +208,64 @@ public void testGsonFutureAdapter() { String json = new Gson().toJson(b); assertThat(json).isEqualTo("{\"f\":{\"i\":2}}"); } -} + + private static class Holder { + @SuppressWarnings("unused") + List field; + } + + /** + * 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 { + var unused = in.nextString(); + return null; + } + }; + + private static WildcardType sampleWildcard() throws Exception { + 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. + return; + } 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 + "]"); + } +} \ No newline at end of file From 066a2dffd0fb37a31c442e9f6264d53230f34a7c Mon Sep 17 00:00:00 2001 From: jaideeppyne Date: Thu, 17 Sep 2026 06:28:45 +0530 Subject: [PATCH 2/3] test: cover blocked runtime reflection deterministically --- .../bind/TypeAdapterRuntimeTypeWrapper.java | 5 ++--- .../TypeAdapterRuntimeTypeWrapperTest.java | 22 ++++++++++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) 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 7fb54d102c..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 @@ -60,8 +60,7 @@ public void write(JsonWriter out, T value) throws IOException { TypeAdapter runtimeTypeAdapter = null; try { @SuppressWarnings("unchecked") - TypeAdapter adapter = - (TypeAdapter) context.getAdapter(TypeToken.get(runtimeType)); + 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 @@ -134,4 +133,4 @@ private static Type getRuntimeTypeIfMoreSpecific(Type type, Object value) { } return type; } -} \ No newline at end of file +} 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 dd0e12ccc2..e004289a54 100644 --- a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java +++ b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java @@ -17,6 +17,7 @@ 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; @@ -26,6 +27,7 @@ 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; @@ -75,6 +77,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. @@ -264,8 +281,7 @@ public void testNonReflectiveInterfaceAdapter_PrefersDelegateWhenRuntimeReflecti assertThat(gson.toJson(new WildcardType[] {wildcard}, WildcardType[].class)) .isEqualTo("[" + expected + "]"); assertThat( - gson.toJson( - Arrays.asList(wildcard), new TypeToken>() {}.getType())) + gson.toJson(Arrays.asList(wildcard), new TypeToken>() {}.getType())) .isEqualTo("[" + expected + "]"); } -} \ No newline at end of file +} From 3f16bcfccd8ed057dd6565fdbc2d9bc2fadbeb72 Mon Sep 17 00:00:00 2001 From: jaideeppyne Date: Sat, 19 Sep 2026 06:38:27 +0530 Subject: [PATCH 3/3] test: tighten runtime wildcard regression setup --- .../TypeAdapterRuntimeTypeWrapperTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 e004289a54..7d696c8d9d 100644 --- a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java +++ b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java @@ -38,6 +38,7 @@ import java.lang.reflect.WildcardType; import java.util.Arrays; import java.util.List; +import org.junit.AssumptionViolatedException; import org.junit.Test; public class TypeAdapterRuntimeTypeWrapperTest { @@ -226,11 +227,6 @@ public void testGsonFutureAdapter() { assertThat(json).isEqualTo("{\"f\":{\"i\":2}}"); } - private static class Holder { - @SuppressWarnings("unused") - List field; - } - /** * A trivial adapter for {@link WildcardType}. It never uses reflection. * @@ -248,12 +244,16 @@ public void write(JsonWriter out, WildcardType value) throws IOException { @Override public WildcardType read(JsonReader in) throws IOException { - var unused = in.nextString(); - return null; + 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]; @@ -268,7 +268,7 @@ public void testNonReflectiveInterfaceAdapter_PrefersDelegateWhenRuntimeReflecti 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. - return; + throw new AssumptionViolatedException("Runtime wildcard type is reflectively accessible"); } catch (JsonIOException expected) { // Continue with the regression assertions. }