diff --git a/UserGuide.md b/UserGuide.md
index ec7fb7ebad..f6bc669fc8 100644
--- a/UserGuide.md
+++ b/UserGuide.md
@@ -155,7 +155,8 @@ BagOfPrimitives obj2 = gson.fromJson(json, BagOfPrimitives.class);
* While serializing, a null field is omitted from the output.
* While deserializing, a missing entry in JSON results in setting the corresponding field in the object to its default value: null for object types, zero for numeric types, and false for booleans.
* If a field is _synthetic_, it is ignored and not included in JSON serialization or deserialization.
-* Fields corresponding to the outer classes in inner classes, anonymous classes, and local classes are ignored and not included in serialization or deserialization.
+* Fields corresponding to the outer classes in inner classes are ignored and not included in serialization or deserialization.
+* Reflection-based serialization and deserialization of anonymous and local classes is not supported; an exception will be thrown. Convert the classes to `static` nested classes or register a custom `TypeAdapter` for them to enable serialization and deserialization.
### Nested Classes (including Inner Classes)
diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java
index e28da7c7f3..aec67198b9 100644
--- a/gson/src/main/java/com/google/gson/GsonBuilder.java
+++ b/gson/src/main/java/com/google/gson/GsonBuilder.java
@@ -291,7 +291,20 @@ public GsonBuilder enableComplexMapKeySerialization() {
}
/**
- * Configures Gson to exclude inner classes during serialization.
+ * Configures Gson to exclude inner classes (= non-{@code static} nested classes) during serialization
+ * and deserialization. This is a convenience method which behaves as if an {@link ExclusionStrategy}
+ * which excludes inner classes was registered with this builder. This means inner classes will be
+ * serialized as JSON {@code null}, and will be deserialized as Java {@code null} with their JSON data
+ * being ignored. And fields with an inner class as type will be ignored during serialization and
+ * deserialization.
+ *
+ *
By default Gson serializes and deserializes inner classes, but ignores references to the
+ * enclosing instance. Deserialization might not be possible at all when {@link #disableJdkUnsafe()}
+ * is used (and no custom {@link InstanceCreator} is registered), or it can lead to unexpected
+ * {@code NullPointerException}s when the deserialized instance is used.
+ *
+ *
In general using inner classes with Gson should be avoided; they should be converted to {@code static}
+ * nested classes if possible.
*
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
* @since 1.3
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 8d8a25f483..827845c190 100644
--- a/gson/src/main/java/com/google/gson/internal/Excluder.java
+++ b/gson/src/main/java/com/google/gson/internal/Excluder.java
@@ -172,10 +172,6 @@ public boolean excludeField(Field field, boolean serialize) {
return true;
}
- if (isAnonymousOrNonStaticLocal(field.getType())) {
- return true;
- }
-
List list = serialize ? serializationStrategies : deserializationStrategies;
if (!list.isEmpty()) {
FieldAttributes fieldAttributes = new FieldAttributes(field);
@@ -198,10 +194,6 @@ private boolean excludeClassChecks(Class> clazz) {
return true;
}
- if (isAnonymousOrNonStaticLocal(clazz)) {
- return true;
- }
-
return false;
}
@@ -220,11 +212,6 @@ private boolean excludeClassInStrategy(Class> clazz, boolean serialize) {
return false;
}
- private boolean isAnonymousOrNonStaticLocal(Class> clazz) {
- return !Enum.class.isAssignableFrom(clazz) && !isStatic(clazz)
- && (clazz.isAnonymousClass() || clazz.isLocalClass());
- }
-
private boolean isInnerClass(Class> clazz) {
return clazz.isMemberClass() && !isStatic(clazz);
}
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 31a44e1a8a..3217d907f2 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
@@ -106,6 +106,13 @@ private List getFieldNames(Field f) {
throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for "
+ raw + ". Register a TypeAdapter for this type or adjust the access filter.");
}
+
+ // Check isStatic to allow serialization for static local classes, e.g. record classes (Java 16+)
+ boolean isAnonymousOrLocal = !Modifier.isStatic(raw.getModifiers()) && (raw.isAnonymousClass() || raw.isLocalClass());
+ if (isAnonymousOrLocal) {
+ return new AnonymousLocalClassAdapter<>(raw);
+ }
+
boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;
ObjectConstructor constructor = constructorConstructor.get(type);
@@ -238,7 +245,14 @@ protected BoundField(String name, boolean serialized, boolean deserialized) {
abstract void read(JsonReader reader, Object value) throws IOException, IllegalAccessException;
}
- public static final class Adapter extends TypeAdapter {
+ /**
+ * Base class for reflection-based adapters; can be tested for to detect when reflection is used to
+ * serialize or deserialize a type.
+ */
+ public abstract static class ReflectiveAdapter extends TypeAdapter {
+ }
+
+ public static class Adapter extends ReflectiveAdapter {
private final ObjectConstructor constructor;
private final Map boundFields;
@@ -292,4 +306,27 @@ public static final class Adapter extends TypeAdapter {
out.endObject();
}
}
+
+ /**
+ * Adapter which throws an exception for anonymous and local classes. These types of classes are problematic
+ * because they might capture values of the enclosing context, which prevents proper deserialization and might
+ * also be missing information on serialization since synthetic fields are ignored by Gson.
+ */
+ static class AnonymousLocalClassAdapter extends ReflectiveAdapter {
+ private final Class> type;
+
+ AnonymousLocalClassAdapter(Class> type) {
+ this.type = type;
+ }
+
+ @Override public void write(JsonWriter out, T value) throws IOException {
+ throw new UnsupportedOperationException("Serialization of anonymous or local class " + type.getName() + " is not supported. "
+ + "Register a TypeAdapter for the class or convert it to a static nested class.");
+ }
+
+ @Override public T read(JsonReader in) throws IOException {
+ throw new UnsupportedOperationException("Deserialization of anonymous or local class " + type.getName() + " is not supported. "
+ + "Register a TypeAdapter for the class or convert it to a static nested class.");
+ }
+ }
}
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 6a6909191d..dc50a33212 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
@@ -53,10 +53,10 @@ public void write(JsonWriter out, T value) throws IOException {
if (runtimeType != type) {
@SuppressWarnings("unchecked")
TypeAdapter runtimeTypeAdapter = (TypeAdapter) context.getAdapter(TypeToken.get(runtimeType));
- if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter)) {
+ if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.ReflectiveAdapter)) {
// The user registered a type adapter for the runtime type, so we will use that
chosen = runtimeTypeAdapter;
- } else if (!(delegate instanceof ReflectiveTypeAdapterFactory.Adapter)) {
+ } else if (!(delegate instanceof ReflectiveTypeAdapterFactory.ReflectiveAdapter)) {
// The user registered a type adapter for Base class, so we prefer it over the
// reflective type adapter for the runtime type
chosen = delegate;
diff --git a/gson/src/test/java/com/google/gson/functional/FieldExclusionTest.java b/gson/src/test/java/com/google/gson/functional/FieldExclusionTest.java
deleted file mode 100644
index 080a8234fe..0000000000
--- a/gson/src/test/java/com/google/gson/functional/FieldExclusionTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2008 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.gson.functional;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-
-import junit.framework.TestCase;
-
-/**
- * Performs some functional testing to ensure GSON infrastructure properly serializes/deserializes
- * fields that either should or should not be included in the output based on the GSON
- * configuration.
- *
- * @author Joel Leitch
- */
-public class FieldExclusionTest extends TestCase {
- private static final String VALUE = "blah_1234";
-
- private Outer outer;
-
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- outer = new Outer();
- }
-
- public void testDefaultInnerClassExclusion() throws Exception {
- Gson gson = new Gson();
- Outer.Inner target = outer.new Inner(VALUE);
- String result = gson.toJson(target);
- assertEquals(target.toJson(), result);
-
- gson = new GsonBuilder().create();
- target = outer.new Inner(VALUE);
- result = gson.toJson(target);
- assertEquals(target.toJson(), result);
- }
-
- public void testInnerClassExclusion() throws Exception {
- Gson gson = new GsonBuilder().disableInnerClassSerialization().create();
- Outer.Inner target = outer.new Inner(VALUE);
- String result = gson.toJson(target);
- assertEquals("null", result);
- }
-
- public void testDefaultNestedStaticClassIncluded() throws Exception {
- Gson gson = new Gson();
- Outer.Inner target = outer.new Inner(VALUE);
- String result = gson.toJson(target);
- assertEquals(target.toJson(), result);
-
- gson = new GsonBuilder().create();
- target = outer.new Inner(VALUE);
- result = gson.toJson(target);
- assertEquals(target.toJson(), result);
- }
-
- private static class Outer {
- private class Inner extends NestedClass {
- public Inner(String value) {
- super(value);
- }
- }
-
- }
-
- private static class NestedClass {
- private final String value;
- public NestedClass(String value) {
- this.value = value;
- }
-
- public String toJson() {
- return "{\"value\":\"" + value + "\"}";
- }
- }
-}
diff --git a/gson/src/test/java/com/google/gson/functional/InnerClassesTest.java b/gson/src/test/java/com/google/gson/functional/InnerClassesTest.java
new file mode 100644
index 0000000000..793e49e1e6
--- /dev/null
+++ b/gson/src/test/java/com/google/gson/functional/InnerClassesTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.gson.functional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import org.junit.Test;
+
+public class InnerClassesTest {
+ private static final String VALUE = "blah_1234";
+
+ private Outer outer = new Outer();
+
+ @Test
+ public void testDefaultInnerClassExclusionSerialization() {
+ Gson gson = new Gson();
+ Outer.Inner target = outer.new Inner(VALUE);
+ String result = gson.toJson(target);
+ assertEquals(target.toJson(), result);
+
+ assertEquals("{\"inner\":" + target.toJson() + "}", gson.toJson(new WithInnerClassField(target)));
+
+ gson = new GsonBuilder().create();
+ target = outer.new Inner(VALUE);
+ result = gson.toJson(target);
+ assertEquals(target.toJson(), result);
+ }
+
+ @Test
+ public void testDefaultInnerClassExclusionDeserialization() {
+ Gson gson = new Gson();
+ Outer.Inner deserialized = gson.fromJson("{\"value\":\"a\"}", Outer.Inner.class);
+ assertNotNull(deserialized);
+ assertEquals("a", deserialized.value);
+
+ WithInnerClassField deserializedWithField = gson.fromJson("{\"inner\":{\"value\":\"a\"}}", WithInnerClassField.class);
+ deserialized = deserializedWithField.inner;
+ assertNotNull(deserialized);
+ assertEquals("a", deserialized.value);
+
+ gson = new GsonBuilder().create();
+ deserialized = gson.fromJson("{\"value\":\"a\"}", Outer.Inner.class);
+ assertNotNull(deserialized);
+ assertEquals("a", deserialized.value);
+ }
+
+ @Test
+ public void testInnerClassExclusionSerialization() {
+ Gson gson = new GsonBuilder().disableInnerClassSerialization().create();
+ Outer.Inner target = outer.new Inner(VALUE);
+ String result = gson.toJson(target);
+ assertEquals("null", result);
+
+ assertEquals("{}", gson.toJson(new WithInnerClassField(target)));
+ }
+
+ @Test
+ public void testInnerClassExclusionDeserialization() {
+ Gson gson = new GsonBuilder().disableInnerClassSerialization().create();
+ Outer.Inner deserialized = gson.fromJson("{\"value\":\"a\"}", Outer.Inner.class);
+ assertNull(deserialized);
+
+ WithInnerClassField deserializedWithField = gson.fromJson("{\"inner\":{\"value\":\"a\"}}", WithInnerClassField.class);
+ deserialized = deserializedWithField.inner;
+ assertNull(deserialized);
+ }
+
+ private static class Outer {
+ private class Inner extends NestedClass {
+ public Inner(String value) {
+ super(value);
+ }
+ }
+ }
+
+ private static class NestedClass {
+ final String value;
+ public NestedClass(String value) {
+ this.value = value;
+ }
+
+ public String toJson() {
+ return "{\"value\":\"" + value + "\"}";
+ }
+ }
+
+ private static class WithInnerClassField {
+ Outer.Inner inner;
+
+ WithInnerClassField(Outer.Inner inner) {
+ this.inner = inner;
+ }
+ }
+}
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 de384879ce..d2e583938c 100644
--- a/gson/src/test/java/com/google/gson/functional/ObjectTest.java
+++ b/gson/src/test/java/com/google/gson/functional/ObjectTest.java
@@ -22,6 +22,7 @@
import com.google.gson.JsonElement;
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;
@@ -285,25 +286,144 @@ public void testPrivateNoArgConstructorDeserialization() throws Exception {
assertEquals(20, target.a);
}
- public void testAnonymousLocalClassesSerialization() throws Exception {
- assertEquals("null", gson.toJson(new ClassWithNoFields() {
+ public void testAnonymousClasses() {
+ Object anonymousClassInstance = new ClassWithNoFields() {
// empty anonymous class
- }));
+ };
+ Class> anonymousClass = anonymousClassInstance.getClass();
+
+ try {
+ gson.toJson(anonymousClassInstance);
+ fail();
+ } catch (UnsupportedOperationException e) {
+ assertEquals("Serialization of anonymous or local class " + anonymousClass.getName() + " is not supported. "
+ + "Register a TypeAdapter for the class or convert it to a static nested class.",
+ e.getMessage());
+ }
+
+ try {
+ gson.fromJson("{}", anonymousClass);
+ fail();
+ } catch (UnsupportedOperationException e) {
+ assertEquals("Deserialization of anonymous or local class " + anonymousClass.getName() + " is not supported. "
+ + "Register a TypeAdapter for the class or convert it to a static nested class.",
+ e.getMessage());
+ }
}
- public void testAnonymousLocalClassesCustomSerialization() throws Exception {
+ public void testAnonymousClassesCustomSerialization() {
gson = new GsonBuilder()
.registerTypeHierarchyAdapter(ClassWithNoFields.class,
new JsonSerializer() {
@Override public JsonElement serialize(
ClassWithNoFields src, Type typeOfSrc, JsonSerializationContext context) {
- return new JsonObject();
+ return new JsonPrimitive("custom-serializer");
}
}).create();
- assertEquals("null", gson.toJson(new ClassWithNoFields() {
+ Object anonymousClassInstance = new ClassWithNoFields() {
// empty anonymous class
- }));
+ };
+ Class> anonymousClass = anonymousClassInstance.getClass();
+
+ assertEquals("\"custom-serializer\"", gson.toJson(anonymousClassInstance));
+
+ // But deserialization should still fail
+ try {
+ gson.fromJson("{}", anonymousClass);
+ fail();
+ } catch (UnsupportedOperationException e) {
+ assertEquals("Deserialization of anonymous or local class " + anonymousClass.getName() + " is not supported. "
+ + "Register a TypeAdapter for the class or convert it to a static nested class.",
+ e.getMessage());
+ }
+ }
+
+ public void testLocalClasses() {
+ class Local extends ClassWithNoFields {
+ }
+
+ try {
+ gson.toJson(new Local());
+ fail();
+ } catch (UnsupportedOperationException e) {
+ assertEquals("Serialization of anonymous or local class " + Local.class.getName() + " is not supported. "
+ + "Register a TypeAdapter for the class or convert it to a static nested class.",
+ e.getMessage());
+ }
+
+ try {
+ gson.fromJson("{}", Local.class);
+ fail();
+ } catch (UnsupportedOperationException e) {
+ assertEquals("Deserialization of anonymous or local class " + Local.class.getName() + " is not supported. "
+ + "Register a TypeAdapter for the class or convert it to a static nested class.",
+ e.getMessage());
+ }
+ }
+
+ public void testLocalClassesCustomSerialization() {
+ gson = new GsonBuilder()
+ .registerTypeHierarchyAdapter(ClassWithNoFields.class,
+ new JsonSerializer() {
+ @Override public JsonElement serialize(
+ ClassWithNoFields src, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive("custom-serializer");
+ }
+ }).create();
+
+ class Local extends ClassWithNoFields {
+ }
+
+ assertEquals("\"custom-serializer\"", gson.toJson(new Local()));
+
+ // But deserialization should still fail
+ try {
+ gson.fromJson("{}", Local.class);
+ fail();
+ } catch (UnsupportedOperationException e) {
+ assertEquals("Deserialization of anonymous or local class " + Local.class.getName() + " is not supported. "
+ + "Register a TypeAdapter for the class or convert it to a static nested class.",
+ e.getMessage());
+ }
+ }
+
+ private static class ClassWithNoFieldsContainer {
+ @SuppressWarnings("unused")
+ ClassWithNoFields f;
+
+ ClassWithNoFieldsContainer(ClassWithNoFields f) {
+ this.f = f;
+ }
+ }
+
+ public void testLocalClassesCustomSerializationForBaseClass() {
+ gson = new GsonBuilder()
+ // Only register adapter for base class
+ .registerTypeAdapter(ClassWithNoFields.class,
+ new JsonSerializer() {
+ @Override public JsonElement serialize(
+ ClassWithNoFields src, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive("custom-serializer");
+ }
+ }).create();
+
+ class Local extends ClassWithNoFields {
+ }
+
+ // TypeAdapterRuntimeTypeWrapper should prefer adapter for base class over reflective
+ // adapter for runtime class (= local class)
+ assertEquals("{\"f\":\"custom-serializer\"}", gson.toJson(new ClassWithNoFieldsContainer(new Local())));
+
+ // But deserialization should still fail
+ try {
+ gson.fromJson("{}", Local.class);
+ fail();
+ } catch (UnsupportedOperationException e) {
+ assertEquals("Deserialization of anonymous or local class " + Local.class.getName() + " is not supported. "
+ + "Register a TypeAdapter for the class or convert it to a static nested class.",
+ e.getMessage());
+ }
}
public void testPrimitiveArrayFieldSerialization() {
diff --git a/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java b/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java
index ece351240a..fa7eadf83e 100644
--- a/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java
+++ b/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java
@@ -8,6 +8,7 @@
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonIOException;
+import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
@@ -113,6 +114,8 @@ public void testSerializeInternalImplementationObject() {
try {
gson.fromJson("{}", internalClass);
fail("Missing exception; test has to be run with `--illegal-access=deny`");
+ } catch (JsonSyntaxException e) {
+ fail("Unexpected exception; test has to be run with `--illegal-access=deny`");
} catch (JsonIOException expected) {
assertTrue(expected.getMessage().startsWith(
"Failed making constructor 'java.util.Collections$EmptyList#EmptyList()' accessible; "