From 72922a67fcf02735c838af0dfe66c40435868b8a Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 23 Sep 2023 21:11:54 +0200 Subject: [PATCH 1/3] Support serializing anonymous and local class with custom adapter --- .../com/google/gson/internal/Excluder.java | 83 +++++++++---------- .../bind/ReflectiveTypeAdapterFactory.java | 21 ++++- .../internal/reflect/ReflectionHelper.java | 12 +++ ...ExposeAnnotationExclusionStrategyTest.java | 30 ++++--- .../gson/InnerClassExclusionStrategyTest.java | 28 ++++++- .../gson/VersionExclusionStrategyTest.java | 57 +++++++++---- .../com/google/gson/functional/EnumTest.java | 8 +- .../google/gson/functional/ObjectTest.java | 57 +++++++++++-- 8 files changed, 212 insertions(+), 84 deletions(-) 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 dd167b4838..f39e41ef0f 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.reflect.ReflectionHelper; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; @@ -109,18 +110,20 @@ public Excluder withExclusionStrategy(ExclusionStrategy exclusionStrategy, @Override public TypeAdapter create(final Gson gson, final TypeToken type) { Class rawType = type.getRawType(); - boolean excludeClass = excludeClassChecks(rawType); - final boolean skipSerialize = excludeClass || excludeClassInStrategy(rawType, true); - final boolean skipDeserialize = excludeClass || excludeClassInStrategy(rawType, false); + final boolean skipSerialize = excludeClass(rawType, true); + final boolean skipDeserialize = excludeClass(rawType, false); if (!skipSerialize && !skipDeserialize) { return null; } return new TypeAdapter() { - /** The delegate is lazily created because it may not be needed, and creating it may fail. */ - private TypeAdapter delegate; + /** + * 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. + */ + private volatile TypeAdapter delegate; @Override public T read(JsonReader in) throws IOException { if (skipDeserialize) { @@ -139,6 +142,7 @@ public Excluder withExclusionStrategy(ExclusionStrategy exclusionStrategy, } private TypeAdapter delegate() { + // A race might lead to `delegate` being assigned by multiple threads but the last assignment will stick TypeAdapter d = delegate; return d != null ? d @@ -168,11 +172,7 @@ public boolean excludeField(Field field, boolean serialize) { } } - if (!serializeInnerClasses && isInnerClass(field.getType())) { - return true; - } - - if (isAnonymousOrNonStaticLocal(field.getType())) { + if (excludeClass(field.getType(), serialize)) { return true; } @@ -189,44 +189,43 @@ public boolean excludeField(Field field, boolean serialize) { return false; } - private boolean excludeClassChecks(Class clazz) { - if (version != Excluder.IGNORE_VERSIONS && !isValidVersion(clazz.getAnnotation(Since.class), clazz.getAnnotation(Until.class))) { - return true; - } - - if (!serializeInnerClasses && isInnerClass(clazz)) { - return true; - } - - return isAnonymousOrNonStaticLocal(clazz); - } - + // public for unit tests; can otherwise be private public boolean excludeClass(Class clazz, boolean serialize) { - return excludeClassChecks(clazz) || - excludeClassInStrategy(clazz, serialize); - } + if (version != Excluder.IGNORE_VERSIONS && !isValidVersion(clazz.getAnnotation(Since.class), clazz.getAnnotation(Until.class))) { + return true; + } - private boolean excludeClassInStrategy(Class clazz, boolean serialize) { - List list = serialize ? serializationStrategies : deserializationStrategies; - for (ExclusionStrategy exclusionStrategy : list) { - if (exclusionStrategy.shouldSkipClass(clazz)) { - return true; - } - } - return false; - } + if (!serializeInnerClasses && isInnerClass(clazz)) { + return true; + } - private boolean isAnonymousOrNonStaticLocal(Class clazz) { - return !Enum.class.isAssignableFrom(clazz) && !isStatic(clazz) - && (clazz.isAnonymousClass() || clazz.isLocalClass()); - } + /* + * Exclude anonymous and local classes because they can have synthetic fields capturing enclosing + * values which makes serialization and deserialization unreliable. + * Don't exclude anonymous enum subclasses because enum types have a built-in adapter. + * + * Exclude only for deserialization; for serialization allow because custom adapter might be + * used; if no custom adapter exists reflection-based adapter otherwise excludes value. + * + * Cannot allow deserialization reliably here because some custom adapters like Collection adapter + * fall back to creating instances using Unsafe, which would likely lead to runtime exceptions + * for anonymous and local classes if they capture values. + */ + if (!serialize && !Enum.class.isAssignableFrom(clazz) && ReflectionHelper.isAnonymousOrNonStaticLocal(clazz)) { + return true; + } - private boolean isInnerClass(Class clazz) { - return clazz.isMemberClass() && !isStatic(clazz); + List list = serialize ? serializationStrategies : deserializationStrategies; + for (ExclusionStrategy exclusionStrategy : list) { + if (exclusionStrategy.shouldSkipClass(clazz)) { + return true; + } + } + return false; } - private boolean isStatic(Class clazz) { - return (clazz.getModifiers() & Modifier.STATIC) != 0; + private static boolean isInnerClass(Class clazz) { + return clazz.isMemberClass() && !ReflectionHelper.isStatic(clazz); } private boolean isValidVersion(Since since, Until until) { diff --git a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java index d981f2c5fd..d901a197dd 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java @@ -78,7 +78,7 @@ public ReflectiveTypeAdapterFactory(ConstructorConstructor constructorConstructo } private boolean includeField(Field f, boolean serialize) { - return !excluder.excludeClass(f.getType(), serialize) && !excluder.excludeField(f, serialize); + return !excluder.excludeField(f, serialize); } /** first element holds the default name */ @@ -110,6 +110,25 @@ public TypeAdapter create(Gson gson, final TypeToken type) { return null; // it's a primitive! } + // Don't allow using reflection on anonymous and local classes because synthetic fields for + // captured enclosing values make this unreliable + if (ReflectionHelper.isAnonymousOrNonStaticLocal(raw)) { + // This adapter just serializes and deserializes null, ignoring the actual values + // This is done for backward compatibility; troubleshooting-wise it might be better to throw exceptions + return new TypeAdapter() { + @Override + public T read(JsonReader in) throws IOException { + in.skipValue(); + return null; + } + + @Override + public void write(JsonWriter out, T value) throws IOException { + out.nullValue(); + } + }; + } + FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw); if (filterResult == FilterResult.BLOCK_ALL) { diff --git a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java index 0345292523..4cc7d71b4d 100644 --- a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java +++ b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java @@ -23,6 +23,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; public class ReflectionHelper { @@ -139,6 +140,17 @@ private static void appendExecutableParameters(AccessibleObject executable, Stri stringBuilder.append(')'); } + public static boolean isStatic(Class clazz) { + return (clazz.getModifiers() & Modifier.STATIC) != 0; + } + + /** + * Returns whether the class is anonymous or a non-static local class. + */ + public static boolean isAnonymousOrNonStaticLocal(Class clazz) { + return !isStatic(clazz) && (clazz.isAnonymousClass() || clazz.isLocalClass()); + } + /** * Tries making the constructor accessible, returning an exception message * if this fails. diff --git a/gson/src/test/java/com/google/gson/ExposeAnnotationExclusionStrategyTest.java b/gson/src/test/java/com/google/gson/ExposeAnnotationExclusionStrategyTest.java index 17aca0361e..228f6293c7 100644 --- a/gson/src/test/java/com/google/gson/ExposeAnnotationExclusionStrategyTest.java +++ b/gson/src/test/java/com/google/gson/ExposeAnnotationExclusionStrategyTest.java @@ -31,38 +31,48 @@ public class ExposeAnnotationExclusionStrategyTest { private Excluder excluder = Excluder.DEFAULT.excludeFieldsWithoutExposeAnnotation(); + private void assertIncludesClass(Class c) { + assertThat(excluder.excludeClass(c, true)).isFalse(); + assertThat(excluder.excludeClass(c, false)).isFalse(); + } + + private void assertIncludesField(Field f) { + assertThat(excluder.excludeField(f, true)).isFalse(); + assertThat(excluder.excludeField(f, false)).isFalse(); + } + + private void assertExcludesField(Field f) { + assertThat(excluder.excludeField(f, true)).isTrue(); + assertThat(excluder.excludeField(f, false)).isTrue(); + } + @Test public void testNeverSkipClasses() { - assertThat(excluder.excludeClass(MockObject.class, true)).isFalse(); - assertThat(excluder.excludeClass(MockObject.class, false)).isFalse(); + assertIncludesClass(MockObject.class); } @Test public void testSkipNonAnnotatedFields() throws Exception { Field f = createFieldAttributes("hiddenField"); - assertThat(excluder.excludeField(f, true)).isTrue(); - assertThat(excluder.excludeField(f, false)).isTrue(); + assertExcludesField(f); } @Test public void testSkipExplicitlySkippedFields() throws Exception { Field f = createFieldAttributes("explicitlyHiddenField"); - assertThat(excluder.excludeField(f, true)).isTrue(); - assertThat(excluder.excludeField(f, false)).isTrue(); + assertExcludesField(f); } @Test public void testNeverSkipExposedAnnotatedFields() throws Exception { Field f = createFieldAttributes("exposedField"); - assertThat(excluder.excludeField(f, true)).isFalse(); - assertThat(excluder.excludeField(f, false)).isFalse(); + assertIncludesField(f); } @Test public void testNeverSkipExplicitlyExposedAnnotatedFields() throws Exception { Field f = createFieldAttributes("explicitlyExposedField"); - assertThat(excluder.excludeField(f, true)).isFalse(); - assertThat(excluder.excludeField(f, false)).isFalse(); + assertIncludesField(f); } @Test diff --git a/gson/src/test/java/com/google/gson/InnerClassExclusionStrategyTest.java b/gson/src/test/java/com/google/gson/InnerClassExclusionStrategyTest.java index 1059f779b6..679222bbb7 100644 --- a/gson/src/test/java/com/google/gson/InnerClassExclusionStrategyTest.java +++ b/gson/src/test/java/com/google/gson/InnerClassExclusionStrategyTest.java @@ -32,28 +32,48 @@ public class InnerClassExclusionStrategyTest { public StaticNestedClass staticNestedClass = new StaticNestedClass(); private Excluder excluder = Excluder.DEFAULT.disableInnerClassSerialization(); + private void assertIncludesClass(Class c) { + assertThat(excluder.excludeClass(c, true)).isFalse(); + assertThat(excluder.excludeClass(c, false)).isFalse(); + } + + private void assertExcludesClass(Class c) { + assertThat(excluder.excludeClass(c, true)).isTrue(); + assertThat(excluder.excludeClass(c, false)).isTrue(); + } + + private void assertIncludesField(Field f) { + assertThat(excluder.excludeField(f, true)).isFalse(); + assertThat(excluder.excludeField(f, false)).isFalse(); + } + + private void assertExcludesField(Field f) { + assertThat(excluder.excludeField(f, true)).isTrue(); + assertThat(excluder.excludeField(f, false)).isTrue(); + } + @Test public void testExcludeInnerClassObject() { Class clazz = innerClass.getClass(); - assertThat(excluder.excludeClass(clazz, true)).isTrue(); + assertExcludesClass(clazz); } @Test public void testExcludeInnerClassField() throws Exception { Field f = getClass().getField("innerClass"); - assertThat(excluder.excludeField(f, true)).isTrue(); + assertExcludesField(f); } @Test public void testIncludeStaticNestedClassObject() { Class clazz = staticNestedClass.getClass(); - assertThat(excluder.excludeClass(clazz, true)).isFalse(); + assertIncludesClass(clazz); } @Test public void testIncludeStaticNestedClassField() throws Exception { Field f = getClass().getField("staticNestedClass"); - assertThat(excluder.excludeField(f, true)).isFalse(); + assertIncludesField(f); } @SuppressWarnings("ClassCanBeStatic") diff --git a/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java b/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java index 2c64496479..5c2b22ca6c 100644 --- a/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java +++ b/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java @@ -22,6 +22,7 @@ import com.google.gson.annotations.Since; import com.google.gson.annotations.Until; import com.google.gson.internal.Excluder; +import java.lang.reflect.Field; import org.junit.Test; /** @@ -32,44 +33,64 @@ public class VersionExclusionStrategyTest { private static final double VERSION = 5.0D; + private static void assertIncludesClass(Excluder excluder, Class c) { + assertThat(excluder.excludeClass(c, true)).isFalse(); + assertThat(excluder.excludeClass(c, false)).isFalse(); + } + + private static void assertExcludesClass(Excluder excluder, Class c) { + assertThat(excluder.excludeClass(c, true)).isTrue(); + assertThat(excluder.excludeClass(c, false)).isTrue(); + } + + private static void assertIncludesField(Excluder excluder, Field f) { + assertThat(excluder.excludeField(f, true)).isFalse(); + assertThat(excluder.excludeField(f, false)).isFalse(); + } + + private static void assertExcludesField(Excluder excluder, Field f) { + assertThat(excluder.excludeField(f, true)).isTrue(); + assertThat(excluder.excludeField(f, false)).isTrue(); + } + @Test public void testSameVersion() throws Exception { Excluder excluder = Excluder.DEFAULT.withVersion(VERSION); - assertThat(excluder.excludeClass(MockClassSince.class, true)).isFalse(); - assertThat(excluder.excludeField(MockClassSince.class.getField("someField"), true)).isFalse(); + assertIncludesClass(excluder, MockClassSince.class); + assertIncludesField(excluder, MockClassSince.class.getField("someField")); // Until version is exclusive - assertThat(excluder.excludeClass(MockClassUntil.class, true)).isTrue(); - assertThat(excluder.excludeField(MockClassUntil.class.getField("someField"), true)).isTrue(); + assertExcludesClass(excluder, MockClassUntil.class); + assertExcludesField(excluder, MockClassUntil.class.getField("someField")); - assertThat(excluder.excludeClass(MockClassBoth.class, true)).isFalse(); - assertThat(excluder.excludeField(MockClassBoth.class.getField("someField"), true)).isFalse(); + assertIncludesClass(excluder, MockClassBoth.class); + assertIncludesField(excluder, MockClassBoth.class.getField("someField")); } @Test public void testNewerVersion() throws Exception { Excluder excluder = Excluder.DEFAULT.withVersion(VERSION + 5); - assertThat(excluder.excludeClass(MockClassSince.class, true)).isFalse(); - assertThat(excluder.excludeField(MockClassSince.class.getField("someField"), true)).isFalse(); + assertIncludesClass(excluder, MockClassSince.class); + assertIncludesField(excluder, MockClassSince.class.getField("someField")); - assertThat(excluder.excludeClass(MockClassUntil.class, true)).isTrue(); - assertThat(excluder.excludeField(MockClassUntil.class.getField("someField"), true)).isTrue(); + assertExcludesClass(excluder, MockClassUntil.class); + assertExcludesField(excluder, MockClassUntil.class.getField("someField")); - assertThat(excluder.excludeClass(MockClassBoth.class, true)).isTrue(); - assertThat(excluder.excludeField(MockClassBoth.class.getField("someField"), true)).isTrue(); + assertExcludesClass(excluder, MockClassBoth.class); + assertExcludesField(excluder, MockClassBoth.class.getField("someField")); } @Test public void testOlderVersion() throws Exception { Excluder excluder = Excluder.DEFAULT.withVersion(VERSION - 5); - assertThat(excluder.excludeClass(MockClassSince.class, true)).isTrue(); - assertThat(excluder.excludeField(MockClassSince.class.getField("someField"), true)).isTrue(); + assertExcludesClass(excluder, MockClassSince.class); + assertExcludesField(excluder, MockClassSince.class.getField("someField")); - assertThat(excluder.excludeClass(MockClassUntil.class, true)).isFalse(); - assertThat(excluder.excludeField(MockClassUntil.class.getField("someField"), true)).isFalse(); + assertIncludesClass(excluder, MockClassUntil.class); + assertIncludesField(excluder, MockClassUntil.class.getField("someField")); - assertThat(excluder.excludeClass(MockClassBoth.class, true)).isTrue(); - assertThat(excluder.excludeField(MockClassBoth.class.getField("someField"), true)).isTrue(); + assertExcludesClass(excluder, MockClassBoth.class); + assertExcludesField(excluder, MockClassBoth.class.getField("someField")); } @Since(VERSION) diff --git a/gson/src/test/java/com/google/gson/functional/EnumTest.java b/gson/src/test/java/com/google/gson/functional/EnumTest.java index 5103a045c0..ab90896938 100644 --- a/gson/src/test/java/com/google/gson/functional/EnumTest.java +++ b/gson/src/test/java/com/google/gson/functional/EnumTest.java @@ -129,6 +129,9 @@ public void testEnumSubclass() { assertThat(EnumSet.allOf(Roshambo.class)).isEqualTo( gson.fromJson("[\"ROCK\",\"PAPER\",\"SCISSORS\"]", new TypeToken>() {}.getType()) ); + + // A bit contrived, but should also work if explicitly deserializing using anonymous enum subclass + assertThat(gson.fromJson("\"ROCK\"", Roshambo.ROCK.getClass())).isEqualTo(Roshambo.ROCK); } @Test @@ -141,9 +144,8 @@ public void testEnumSubclassWithRegisteredTypeAdapter() { assertThat(gson.toJson(Roshambo.ROCK)).isEqualTo("\"123ROCK\""); assertThat(gson.toJson(EnumSet.allOf(Roshambo.class))).isEqualTo("[\"123ROCK\",\"123PAPER\",\"123SCISSORS\"]"); assertThat(gson.fromJson("\"123ROCK\"", Roshambo.class)).isEqualTo(Roshambo.ROCK); - assertThat(EnumSet.allOf(Roshambo.class)).isEqualTo( - gson.fromJson("[\"123ROCK\",\"123PAPER\",\"123SCISSORS\"]", new TypeToken>() {}.getType()) - ); + Set deserialized = gson.fromJson("[\"123ROCK\",\"123PAPER\",\"123SCISSORS\"]", new TypeToken>() {}.getType()); + assertThat(deserialized).isEqualTo(EnumSet.allOf(Roshambo.class)); } @Test diff --git a/gson/src/test/java/com/google/gson/functional/ObjectTest.java b/gson/src/test/java/com/google/gson/functional/ObjectTest.java index e0efd94dee..21366ed7ae 100644 --- a/gson/src/test/java/com/google/gson/functional/ObjectTest.java +++ b/gson/src/test/java/com/google/gson/functional/ObjectTest.java @@ -22,10 +22,13 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; 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.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.google.gson.common.TestTypes.ArrayOfObjects; @@ -37,7 +40,6 @@ import com.google.gson.common.TestTypes.ClassWithTransientFields; import com.google.gson.common.TestTypes.Nested; import com.google.gson.common.TestTypes.PrimitiveArray; -import com.google.gson.internal.JavaVersion; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.util.ArrayList; @@ -349,22 +351,65 @@ public void testAnonymousLocalClassesSerialization() { assertThat(gson.toJson(new ClassWithNoFields() { // empty anonymous class })).isEqualTo("null"); + + class Local {} + assertThat(gson.toJson(new Local())).isEqualTo("null"); } @Test public void testAnonymousLocalClassesCustomSerialization() { - gson = new GsonBuilder() + Gson gson = new GsonBuilder() .registerTypeHierarchyAdapter(ClassWithNoFields.class, new JsonSerializer() { - @Override public JsonElement serialize( - ClassWithNoFields src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonObject(); + @Override + public JsonElement serialize(ClassWithNoFields src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("custom-value"); } }).create(); assertThat(gson.toJson(new ClassWithNoFields() { // empty anonymous class - })).isEqualTo("null"); + })).isEqualTo("\"custom-value\""); + + class Local {} + gson = new GsonBuilder() + .registerTypeAdapter(Local.class, + new JsonSerializer() { + @Override + public JsonElement serialize(Local src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("custom-value"); + } + }).create(); + assertThat(gson.toJson(new Local())).isEqualTo("\"custom-value\""); + } + + @Test + public void testAnonymousLocalClassesCustomDeserialization() { + Gson gson = new GsonBuilder() + .registerTypeHierarchyAdapter(ClassWithNoFields.class, + new JsonDeserializer() { + @Override + public ClassWithNoFields deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + return new ClassWithNoFields(); + } + }).create(); + + assertThat(gson.fromJson("{}", ClassWithNoFields.class)).isNotNull(); + Class anonymousClass = new ClassWithNoFields() {}.getClass(); + // Custom deserializer is ignored + assertThat(gson.fromJson("{}", anonymousClass)).isNull(); + + class Local {} + gson = new GsonBuilder() + .registerTypeAdapter(Local.class, + new JsonDeserializer() { + @Override + public Local deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + throw new AssertionError("should not be called"); + } + }).create(); + // Custom deserializer is ignored + assertThat(gson.fromJson("{}", Local.class)).isNull(); } @Test From af1a800e7cd2ba5df2155134529f8c5f408a0971 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 19 Nov 2023 15:55:01 +0100 Subject: [PATCH 2/3] Fix formatting and fix switched 'expected' and 'actual' in EnumTest --- .../com/google/gson/internal/Excluder.java | 42 +++++---- .../bind/ReflectiveTypeAdapterFactory.java | 3 +- .../internal/reflect/ReflectionHelper.java | 4 +- .../com/google/gson/functional/EnumTest.java | 13 +-- .../google/gson/functional/ObjectTest.java | 91 +++++++++++-------- 5 files changed, 88 insertions(+), 65 deletions(-) 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 f39e41ef0f..9a6ba9db34 100644 --- a/gson/src/main/java/com/google/gson/internal/Excluder.java +++ b/gson/src/main/java/com/google/gson/internal/Excluder.java @@ -36,14 +36,12 @@ import java.util.List; /** - * This class selects which fields and types to omit. It is configurable, - * supporting version attributes {@link Since} and {@link Until}, modifiers, - * synthetic fields, anonymous and local classes, inner classes, and fields with - * the {@link Expose} annotation. + * This class selects which fields and types to omit. It is configurable, supporting version + * attributes {@link Since} and {@link Until}, modifiers, synthetic fields, anonymous and local + * classes, inner classes, and fields with the {@link Expose} annotation. * - *

This class is a type adapter factory; types that are excluded will be - * adapted to null. It may delegate to another type adapter if only one - * direction is excluded. + *

This class is a type adapter factory; types that are excluded will be adapted to null. It may + * delegate to another type adapter if only one direction is excluded. * * @author Joel Leitch * @author Jesse Wilson @@ -59,7 +57,8 @@ public final class Excluder implements TypeAdapterFactory, Cloneable { private List serializationStrategies = Collections.emptyList(); private List deserializationStrategies = Collections.emptyList(); - @Override protected Excluder clone() { + @Override + protected Excluder clone() { try { return (Excluder) super.clone(); } catch (CloneNotSupportedException e) { @@ -94,8 +93,8 @@ public Excluder excludeFieldsWithoutExposeAnnotation() { return result; } - public Excluder withExclusionStrategy(ExclusionStrategy exclusionStrategy, - boolean serialization, boolean deserialization) { + public Excluder withExclusionStrategy( + ExclusionStrategy exclusionStrategy, boolean serialization, boolean deserialization) { Excluder result = clone(); if (serialization) { result.serializationStrategies = new ArrayList<>(serializationStrategies); @@ -108,7 +107,8 @@ public Excluder withExclusionStrategy(ExclusionStrategy exclusionStrategy, return result; } - @Override public TypeAdapter create(final Gson gson, final TypeToken type) { + @Override + public TypeAdapter create(final Gson gson, final TypeToken type) { Class rawType = type.getRawType(); final boolean skipSerialize = excludeClass(rawType, true); @@ -125,7 +125,8 @@ public Excluder withExclusionStrategy(ExclusionStrategy exclusionStrategy, */ private volatile TypeAdapter delegate; - @Override public T read(JsonReader in) throws IOException { + @Override + public T read(JsonReader in) throws IOException { if (skipDeserialize) { in.skipValue(); return null; @@ -133,7 +134,8 @@ public Excluder withExclusionStrategy(ExclusionStrategy exclusionStrategy, return delegate().read(in); } - @Override public void write(JsonWriter out, T value) throws IOException { + @Override + public void write(JsonWriter out, T value) throws IOException { if (skipSerialize) { out.nullValue(); return; @@ -142,11 +144,10 @@ public Excluder withExclusionStrategy(ExclusionStrategy exclusionStrategy, } private TypeAdapter delegate() { - // A race might lead to `delegate` being assigned by multiple threads but the last assignment will stick + // A race might lead to `delegate` being assigned by multiple threads but the last + // assignment will stick TypeAdapter d = delegate; - return d != null - ? d - : (delegate = gson.getDelegateAdapter(Excluder.this, type)); + return d != null ? d : (delegate = gson.getDelegateAdapter(Excluder.this, type)); } }; } @@ -191,7 +192,8 @@ public boolean excludeField(Field field, boolean serialize) { // public for unit tests; can otherwise be private public boolean excludeClass(Class clazz, boolean serialize) { - if (version != Excluder.IGNORE_VERSIONS && !isValidVersion(clazz.getAnnotation(Since.class), clazz.getAnnotation(Until.class))) { + if (version != Excluder.IGNORE_VERSIONS + && !isValidVersion(clazz.getAnnotation(Since.class), clazz.getAnnotation(Until.class))) { return true; } @@ -211,7 +213,9 @@ public boolean excludeClass(Class clazz, boolean serialize) { * fall back to creating instances using Unsafe, which would likely lead to runtime exceptions * for anonymous and local classes if they capture values. */ - if (!serialize && !Enum.class.isAssignableFrom(clazz) && ReflectionHelper.isAnonymousOrNonStaticLocal(clazz)) { + if (!serialize + && !Enum.class.isAssignableFrom(clazz) + && ReflectionHelper.isAnonymousOrNonStaticLocal(clazz)) { return true; } diff --git a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java index 25657fee58..a2a8412e57 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java @@ -114,7 +114,8 @@ public TypeAdapter create(Gson gson, final TypeToken type) { // captured enclosing values make this unreliable if (ReflectionHelper.isAnonymousOrNonStaticLocal(raw)) { // This adapter just serializes and deserializes null, ignoring the actual values - // This is done for backward compatibility; troubleshooting-wise it might be better to throw exceptions + // This is done for backward compatibility; troubleshooting-wise it might be better to throw + // exceptions return new TypeAdapter() { @Override public T read(JsonReader in) throws IOException { diff --git a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java index b2e22a4daf..285396bd2e 100644 --- a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java +++ b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java @@ -151,9 +151,7 @@ public static boolean isStatic(Class clazz) { return (clazz.getModifiers() & Modifier.STATIC) != 0; } - /** - * Returns whether the class is anonymous or a non-static local class. - */ + /** Returns whether the class is anonymous or a non-static local class. */ public static boolean isAnonymousOrNonStaticLocal(Class clazz) { return !isStatic(clazz) && (clazz.isAnonymousClass() || clazz.isLocalClass()); } diff --git a/gson/src/test/java/com/google/gson/functional/EnumTest.java b/gson/src/test/java/com/google/gson/functional/EnumTest.java index 68205a610d..835fb5086b 100644 --- a/gson/src/test/java/com/google/gson/functional/EnumTest.java +++ b/gson/src/test/java/com/google/gson/functional/EnumTest.java @@ -127,12 +127,12 @@ public void testEnumSubclass() { assertThat(gson.toJson(EnumSet.allOf(Roshambo.class))) .isEqualTo("[\"ROCK\",\"PAPER\",\"SCISSORS\"]"); assertThat(gson.fromJson("\"ROCK\"", Roshambo.class)).isEqualTo(Roshambo.ROCK); - assertThat(EnumSet.allOf(Roshambo.class)) - .isEqualTo( - gson.fromJson( - "[\"ROCK\",\"PAPER\",\"SCISSORS\"]", new TypeToken>() {}.getType())); + Set deserialized = + gson.fromJson("[\"ROCK\",\"PAPER\",\"SCISSORS\"]", new TypeToken<>() {}); + assertThat(deserialized).isEqualTo(EnumSet.allOf(Roshambo.class)); - // A bit contrived, but should also work if explicitly deserializing using anonymous enum subclass + // A bit contrived, but should also work if explicitly deserializing using anonymous enum + // subclass assertThat(gson.fromJson("\"ROCK\"", Roshambo.ROCK.getClass())).isEqualTo(Roshambo.ROCK); } @@ -148,7 +148,8 @@ public void testEnumSubclassWithRegisteredTypeAdapter() { assertThat(gson.toJson(EnumSet.allOf(Roshambo.class))) .isEqualTo("[\"123ROCK\",\"123PAPER\",\"123SCISSORS\"]"); assertThat(gson.fromJson("\"123ROCK\"", Roshambo.class)).isEqualTo(Roshambo.ROCK); - Set deserialized = gson.fromJson("[\"123ROCK\",\"123PAPER\",\"123SCISSORS\"]", new TypeToken>() {}.getType()); + Set deserialized = + gson.fromJson("[\"123ROCK\",\"123PAPER\",\"123SCISSORS\"]", new TypeToken<>() {}); assertThat(deserialized).isEqualTo(EnumSet.allOf(Roshambo.class)); } diff --git a/gson/src/test/java/com/google/gson/functional/ObjectTest.java b/gson/src/test/java/com/google/gson/functional/ObjectTest.java index 6d6b683e88..d28a255a43 100644 --- a/gson/src/test/java/com/google/gson/functional/ObjectTest.java +++ b/gson/src/test/java/com/google/gson/functional/ObjectTest.java @@ -391,41 +391,56 @@ class Local {} @Test public void testAnonymousLocalClassesCustomSerialization() { - Gson gson = new GsonBuilder() - .registerTypeHierarchyAdapter(ClassWithNoFields.class, - new JsonSerializer() { - @Override - public JsonElement serialize(ClassWithNoFields src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive("custom-value"); - } - }).create(); - - assertThat(gson.toJson(new ClassWithNoFields() { - // empty anonymous class - })).isEqualTo("\"custom-value\""); + Gson gson = + new GsonBuilder() + .registerTypeHierarchyAdapter( + ClassWithNoFields.class, + new JsonSerializer() { + @Override + public JsonElement serialize( + ClassWithNoFields src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("custom-value"); + } + }) + .create(); + + assertThat( + gson.toJson( + new ClassWithNoFields() { + // empty anonymous class + })) + .isEqualTo("\"custom-value\""); class Local {} - gson = new GsonBuilder() - .registerTypeAdapter(Local.class, - new JsonSerializer() { - @Override - public JsonElement serialize(Local src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive("custom-value"); - } - }).create(); + gson = + new GsonBuilder() + .registerTypeAdapter( + Local.class, + new JsonSerializer() { + @Override + public JsonElement serialize( + Local src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("custom-value"); + } + }) + .create(); assertThat(gson.toJson(new Local())).isEqualTo("\"custom-value\""); } @Test public void testAnonymousLocalClassesCustomDeserialization() { - Gson gson = new GsonBuilder() - .registerTypeHierarchyAdapter(ClassWithNoFields.class, - new JsonDeserializer() { - @Override - public ClassWithNoFields deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { - return new ClassWithNoFields(); - } - }).create(); + Gson gson = + new GsonBuilder() + .registerTypeHierarchyAdapter( + ClassWithNoFields.class, + new JsonDeserializer() { + @Override + public ClassWithNoFields deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) { + return new ClassWithNoFields(); + } + }) + .create(); assertThat(gson.fromJson("{}", ClassWithNoFields.class)).isNotNull(); Class anonymousClass = new ClassWithNoFields() {}.getClass(); @@ -433,14 +448,18 @@ public ClassWithNoFields deserialize(JsonElement json, Type typeOfT, JsonDeseria assertThat(gson.fromJson("{}", anonymousClass)).isNull(); class Local {} - gson = new GsonBuilder() - .registerTypeAdapter(Local.class, - new JsonDeserializer() { - @Override - public Local deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { - throw new AssertionError("should not be called"); - } - }).create(); + gson = + new GsonBuilder() + .registerTypeAdapter( + Local.class, + new JsonDeserializer() { + @Override + public Local deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) { + throw new AssertionError("should not be called"); + } + }) + .create(); // Custom deserializer is ignored assertThat(gson.fromJson("{}", Local.class)).isNull(); } From fdc1a761a53d8fff18a9de9a8042758bafbd96dd Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 19 Nov 2023 16:13:49 +0100 Subject: [PATCH 3/3] Minor code improvements --- .../gson/internal/bind/ReflectiveTypeAdapterFactory.java | 5 +++++ .../com/google/gson/internal/reflect/ReflectionHelper.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java index a2a8412e57..b7f8393ddb 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java @@ -127,6 +127,11 @@ public T read(JsonReader in) throws IOException { public void write(JsonWriter out, T value) throws IOException { out.nullValue(); } + + @Override + public String toString() { + return "AnonymousOrNonStaticLocalClassAdapter"; + } }; } diff --git a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java index 285396bd2e..7ee75bde92 100644 --- a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java +++ b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java @@ -148,7 +148,7 @@ private static void appendExecutableParameters( } public static boolean isStatic(Class clazz) { - return (clazz.getModifiers() & Modifier.STATIC) != 0; + return Modifier.isStatic(clazz.getModifiers()); } /** Returns whether the class is anonymous or a non-static local class. */