diff --git a/.github/workflows/check-android-compatibility.yml b/.github/workflows/check-android-compatibility.yml
index 9122d4207c..939582e547 100644
--- a/.github/workflows/check-android-compatibility.yml
+++ b/.github/workflows/check-android-compatibility.yml
@@ -26,4 +26,5 @@ jobs:
- name: Check Android compatibility
run: |
# Run 'test' phase because plugin normally expects to be executed after tests have been compiled
- mvn --batch-mode --no-transfer-progress test animal-sniffer:check@check-android-compatibility -DskipTests
+ # Have to skip 'jpms-test' module because it requires that full Gson JAR has been built
+ mvn --batch-mode --no-transfer-progress test animal-sniffer:check@check-android-compatibility -DskipTests --projects '!jpms-test'
diff --git a/extras/pom.xml b/extras/pom.xml
index 50a65f7136..020ab45f48 100644
--- a/extras/pom.xml
+++ b/extras/pom.xml
@@ -31,6 +31,9 @@
2024-05-19T18:54:10Z
+
+
+ true
@@ -64,21 +67,6 @@
-
-
-
-
- org.apache.maven.plugins
- maven-deploy-plugin
-
-
- true
-
-
-
-
-
-
Inderjeet Singh
diff --git a/graal-native-image-test/pom.xml b/graal-native-image-test/pom.xml
index 7254f2fb10..6f556c1ad6 100644
--- a/graal-native-image-test/pom.xml
+++ b/graal-native-image-test/pom.xml
@@ -14,7 +14,6 @@
limitations under the License.
-->
-
4.0.0
@@ -23,16 +22,16 @@
2.11.1-SNAPSHOT
graal-native-image-test
+ Test: GraalVM Native Image
-
-
- 2024-05-19T18:54:10Z
-
11
**/Java17*
+
+
+ true
@@ -63,22 +62,6 @@
-
- com.github.siom79.japicmp
- japicmp-maven-plugin
-
-
- true
-
-
-
- org.codehaus.mojo
- animal-sniffer-maven-plugin
-
-
- true
-
-
org.apache.maven.plugins
maven-jar-plugin
@@ -87,31 +70,6 @@
true
-
- org.apache.maven.plugins
- maven-install-plugin
-
-
- true
-
-
-
- org.apache.maven.plugins
- maven-deploy-plugin
-
-
- true
-
-
-
- org.apache.maven.plugins
- maven-gpg-plugin
-
-
- true
-
-
diff --git a/gson/src/main/java/module-info.java b/gson/src/main/java/module-info.java
index 1b951ceafa..6692639673 100644
--- a/gson/src/main/java/module-info.java
+++ b/gson/src/main/java/module-info.java
@@ -25,7 +25,7 @@
exports com.google.gson.reflect;
exports com.google.gson.stream;
- // Dependency on Error Prone Annotations
+ // Optional dependency on Error Prone Annotations
requires static com.google.errorprone.annotations;
// Optional dependency on java.sql
diff --git a/jpms-test/README.md b/jpms-test/README.md
new file mode 100644
index 0000000000..c667c0dad1
--- /dev/null
+++ b/jpms-test/README.md
@@ -0,0 +1,5 @@
+# jpms-test
+
+This Maven module contains tests to verify that Gson's `module-info.class` which is used by the Java Platform Module System (JPMS) works properly and can be used by other projects. The module declaration file `src/test/java/module-info.java` uses Gson's module.
+
+This is a separate Maven module to test Gson's final JAR instead of just the compiled classes.
diff --git a/jpms-test/pom.xml b/jpms-test/pom.xml
new file mode 100644
index 0000000000..5589549ad1
--- /dev/null
+++ b/jpms-test/pom.xml
@@ -0,0 +1,53 @@
+
+
+ 4.0.0
+
+
+ com.google.code.gson
+ gson-parent
+ 2.11.1-SNAPSHOT
+
+ jpms-test
+ Test: Java Platform Module System (JPMS)
+
+
+ 11
+ ${maven.compiler.release}
+
+
+ true
+
+
+
+
+ com.google.code.gson
+ gson
+ ${project.parent.version}
+
+
+
+ junit
+ junit
+ test
+
+
+ com.google.truth
+ truth
+ test
+
+
+
diff --git a/jpms-test/src/main/java/module-info.java b/jpms-test/src/main/java/module-info.java
new file mode 100644
index 0000000000..04360c0fc3
--- /dev/null
+++ b/jpms-test/src/main/java/module-info.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+/**
+ * Dummy module to prevent Maven Compiler Plugin from failing if {@code module-info.java} exists
+ * only in test sources:
+ *
+ *
+ *
+ * Can't compile test sources when main sources are missing a module descriptor
+ *
+ *
+ */
+module com.google.gson.dummy {}
diff --git a/jpms-test/src/test/java/com/google/gson/jpms_test/ExportedPackagesTest.java b/jpms-test/src/test/java/com/google/gson/jpms_test/ExportedPackagesTest.java
new file mode 100644
index 0000000000..c30b3fb75e
--- /dev/null
+++ b/jpms-test/src/test/java/com/google/gson/jpms_test/ExportedPackagesTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 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.jpms_test;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InaccessibleObjectException;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import org.junit.Test;
+
+/**
+ * Verifies that Gson's {@code module-info.class} properly 'exports' all its packages containing
+ * public API.
+ */
+public class ExportedPackagesTest {
+ /** Tests package {@code com.google.gson} */
+ @Test
+ public void testMainPackage() {
+ Gson gson = new Gson();
+ assertThat(gson.toJson(1)).isEqualTo("1");
+ }
+
+ /** Tests package {@code com.google.gson.annotations} */
+ @Test
+ public void testAnnotationsPackage() throws Exception {
+ class Annotated {
+ @SerializedName("custom-name")
+ int i;
+ }
+
+ Field field = Annotated.class.getDeclaredField("i");
+ SerializedName annotation = field.getAnnotation(SerializedName.class);
+ assertThat(annotation.value()).isEqualTo("custom-name");
+ }
+
+ /** Tests package {@code com.google.gson.reflect} */
+ @Test
+ public void testReflectPackage() {
+ var typeToken = TypeToken.get(String.class);
+ assertThat(typeToken.getRawType()).isEqualTo(String.class);
+ }
+
+ /** Tests package {@code com.google.gson.stream} */
+ @Test
+ public void testStreamPackage() throws IOException {
+ JsonReader jsonReader = new JsonReader(new StringReader("2"));
+ assertThat(jsonReader.nextInt()).isEqualTo(2);
+ }
+
+ /** Verifies that Gson packages are only 'exported' but not 'opened' for reflection. */
+ @Test
+ public void testReflectionInternalField() throws Exception {
+ Gson gson = new Gson();
+
+ // Get an arbitrary non-public instance field
+ Field field =
+ Arrays.stream(Gson.class.getDeclaredFields())
+ .filter(
+ f -> !Modifier.isStatic(f.getModifiers()) && !Modifier.isPublic(f.getModifiers()))
+ .findFirst()
+ .get();
+ assertThrows(InaccessibleObjectException.class, () -> field.setAccessible(true));
+ assertThrows(IllegalAccessException.class, () -> field.get(gson));
+ }
+
+ @Test
+ public void testInaccessiblePackage() throws Exception {
+ // Note: In case this class is renamed / removed, can change this to any other internal class
+ Class> internalClass = Class.forName("com.google.gson.internal.LinkedTreeMap");
+ assertThat(Modifier.isPublic(internalClass.getModifiers())).isTrue();
+ // Get the public constructor
+ Constructor> constructor = internalClass.getConstructor();
+ assertThrows(InaccessibleObjectException.class, () -> constructor.setAccessible(true));
+ assertThrows(IllegalAccessException.class, () -> constructor.newInstance());
+ }
+}
diff --git a/jpms-test/src/test/java/com/google/gson/jpms_test/ModuleTest.java b/jpms-test/src/test/java/com/google/gson/jpms_test/ModuleTest.java
new file mode 100644
index 0000000000..14e2917b77
--- /dev/null
+++ b/jpms-test/src/test/java/com/google/gson/jpms_test/ModuleTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 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.jpms_test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import java.lang.module.ModuleDescriptor;
+import java.lang.module.ModuleDescriptor.Exports;
+import java.lang.module.ModuleDescriptor.Requires;
+import java.net.URL;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.Test;
+
+/**
+ * Verifies that this test project is properly set up and has a module descriptor, and checks Gson's
+ * module descriptor.
+ */
+public class ModuleTest {
+ @Test
+ public void testOwnModule() {
+ Module module = getClass().getModule();
+ assertThat(module.getName()).isEqualTo("com.google.gson.jpms_test");
+ }
+
+ @Test
+ public void testGsonModule() {
+ // Verify that this test actually loads the final Gson JAR, and not only compiled classes
+ // Note: This might fail when run from the IDE, but should succeed when run with Maven from
+ // command line
+ URL gsonLocation = Gson.class.getProtectionDomain().getCodeSource().getLocation();
+ assertThat(gsonLocation.getPath()).containsMatch("gson/target/gson-[^/]+\\.jar");
+
+ Module module = Gson.class.getModule();
+ ModuleDescriptor moduleDescriptor = module.getDescriptor();
+
+ assertThat(moduleDescriptor.name()).isEqualTo("com.google.gson");
+ // Permit `Modifier.SYNTHETIC`; current versions of Moditect seem to set that
+ assertThat(moduleDescriptor.modifiers())
+ .containsNoneOf(
+ ModuleDescriptor.Modifier.AUTOMATIC,
+ ModuleDescriptor.Modifier.MANDATED,
+ ModuleDescriptor.Modifier.OPEN);
+ // Should have implicitly included the Maven project version
+ assertThat(moduleDescriptor.rawVersion()).isPresent();
+
+ Set moduleRequires = moduleDescriptor.requires();
+ assertThat(getModuleDependencies(moduleRequires))
+ .containsExactly("com.google.errorprone.annotations", "java.sql", "jdk.unsupported");
+ assertThat(getTransitiveModuleDependencies(moduleRequires)).isEmpty();
+ assertThat(getOptionalModuleDependencies(moduleRequires))
+ .containsExactly("com.google.errorprone.annotations", "java.sql", "jdk.unsupported");
+
+ Set packageExports = moduleDescriptor.exports();
+ assertThat(packageExports.stream().map(Exports::source))
+ .containsExactly(
+ "com.google.gson",
+ "com.google.gson.annotations",
+ "com.google.gson.reflect",
+ "com.google.gson.stream");
+ // Gson currently does not export packages to specific modules only
+ assertThat(packageExports.stream().filter(Exports::isQualified)).isEmpty();
+
+ // Gson does not allow access to its implementation details using reflection
+ assertThat(moduleDescriptor.opens()).isEmpty();
+ // Gson currently does not use or provide any services
+ assertThat(moduleDescriptor.uses()).isEmpty();
+ assertThat(moduleDescriptor.provides()).isEmpty();
+ // Gson has no main class
+ assertThat(moduleDescriptor.mainClass()).isEmpty();
+ }
+
+ private static Stream filterImplicitRequires(Set requires) {
+ return requires.stream()
+ .filter(
+ r ->
+ !r.modifiers().contains(Requires.Modifier.MANDATED)
+ && !r.modifiers().contains(Requires.Modifier.SYNTHETIC));
+ }
+
+ private static Set getModuleDependencies(Set requires) {
+ return filterImplicitRequires(requires).map(Requires::name).collect(Collectors.toSet());
+ }
+
+ private static Set getTransitiveModuleDependencies(Set requires) {
+ return filterImplicitRequires(requires)
+ .filter(r -> r.modifiers().contains(Requires.Modifier.TRANSITIVE))
+ .map(Requires::name)
+ .collect(Collectors.toSet());
+ }
+
+ private static Set getOptionalModuleDependencies(Set requires) {
+ return filterImplicitRequires(requires)
+ .filter(r -> r.modifiers().contains(Requires.Modifier.STATIC))
+ .map(Requires::name)
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/jpms-test/src/test/java/com/google/gson/jpms_test/ReflectionInaccessibleTest.java b/jpms-test/src/test/java/com/google/gson/jpms_test/ReflectionInaccessibleTest.java
new file mode 100644
index 0000000000..0358b78df9
--- /dev/null
+++ b/jpms-test/src/test/java/com/google/gson/jpms_test/ReflectionInaccessibleTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 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.jpms_test;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonIOException;
+import org.junit.Test;
+
+/**
+ * Verifies that Gson cannot use reflection for classes in a package if it has not been 'opened' to
+ * the Gson module in the {@code module-info.java} of the project.
+ */
+public class ReflectionInaccessibleTest {
+ private static class MyClass {
+ @SuppressWarnings("unused")
+ int i;
+ }
+
+ @Test
+ public void testDeserialization() {
+ Gson gson = new Gson();
+ var e = assertThrows(JsonIOException.class, () -> gson.fromJson("{\"i\":1}", MyClass.class));
+ assertThat(e)
+ .hasMessageThat()
+ .isEqualTo(
+ "Failed making field '"
+ + MyClass.class.getName()
+ + "#i' accessible; either increase its visibility or write a custom TypeAdapter for"
+ + " its declaring type.\n"
+ + "See https://github.com/google/gson/blob/main/Troubleshooting.md#reflection-inaccessible-to-module-gson");
+ }
+
+ @Test
+ public void testSerialization() {
+ Gson gson = new Gson();
+
+ MyClass obj = new MyClass();
+ obj.i = 1;
+ var e = assertThrows(JsonIOException.class, () -> gson.toJson(obj));
+ assertThat(e)
+ .hasMessageThat()
+ .isEqualTo(
+ "Failed making field '"
+ + MyClass.class.getName()
+ + "#i' accessible; either increase its visibility or write a custom TypeAdapter for"
+ + " its declaring type.\n"
+ + "See https://github.com/google/gson/blob/main/Troubleshooting.md#reflection-inaccessible-to-module-gson");
+ }
+}
diff --git a/jpms-test/src/test/java/com/google/gson/jpms_test/opened/ReflectionTest.java b/jpms-test/src/test/java/com/google/gson/jpms_test/opened/ReflectionTest.java
new file mode 100644
index 0000000000..140472cfa8
--- /dev/null
+++ b/jpms-test/src/test/java/com/google/gson/jpms_test/opened/ReflectionTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+// This package is opened for reflection, see `module-info.java`
+package com.google.gson.jpms_test.opened;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import org.junit.Test;
+
+/**
+ * Verifies that Gson can use reflection for classes in a package if it has been 'opened' to the
+ * Gson module in the {@code module-info.java} of the project.
+ */
+public class ReflectionTest {
+ private static class MyClass {
+ int i;
+ }
+
+ @Test
+ public void testDeserialization() {
+ Gson gson = new Gson();
+ MyClass deserialized = gson.fromJson("{\"i\":1}", MyClass.class);
+ assertThat(deserialized.i).isEqualTo(1);
+ }
+
+ @Test
+ public void testSerialization() {
+ Gson gson = new Gson();
+
+ MyClass obj = new MyClass();
+ obj.i = 1;
+ String serialized = gson.toJson(obj);
+ assertThat(serialized).isEqualTo("{\"i\":1}");
+ }
+}
diff --git a/jpms-test/src/test/java/module-info.java b/jpms-test/src/test/java/module-info.java
new file mode 100644
index 0000000000..b4ecf2d1f4
--- /dev/null
+++ b/jpms-test/src/test/java/module-info.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+@SuppressWarnings("requires-automatic") // for automatic module names for 'junit' and 'truth'
+module com.google.gson.jpms_test {
+ requires com.google.gson;
+
+ // Test dependencies
+ requires junit;
+ requires truth; // has no proper module name yet, see https://github.com/google/truth/issues/605
+
+ opens com.google.gson.jpms_test to
+ junit;
+ opens com.google.gson.jpms_test.opened to
+ junit,
+ com.google.gson;
+}
diff --git a/metrics/pom.xml b/metrics/pom.xml
index 325ab891db..09091795f4 100644
--- a/metrics/pom.xml
+++ b/metrics/pom.xml
@@ -28,9 +28,8 @@
Performance Metrics for Google Gson library
-
-
- 2024-05-19T18:54:10Z
+
+ true
@@ -63,37 +62,6 @@
-
-
-
-
- com.github.siom79.japicmp
- japicmp-maven-plugin
-
-
- true
-
-
-
- org.codehaus.mojo
- animal-sniffer-maven-plugin
-
-
- true
-
-
-
- org.apache.maven.plugins
- maven-deploy-plugin
-
-
- true
-
-
-
-
-
-
Inderjeet Singh
diff --git a/pom.xml b/pom.xml
index 61fdf48279..f755076592 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,6 +28,7 @@
gson
+ jpms-test
graal-native-image-test
shrinker-test
extras
@@ -43,6 +44,12 @@
2024-05-19T18:54:10Z
+
+
+
+ false
+
+ ${gson.isTestModule}
11
@@ -340,16 +349,33 @@
org.apache.maven.plugins
maven-install-plugin
3.1.2
+
+ ${gson.isTestModule}
+
org.apache.maven.plugins
maven-source-plugin
3.3.1
+
+ ${gson.isTestModule}
+
org.apache.maven.plugins
maven-gpg-plugin
3.2.4
+
+ ${gson.isTestModule}
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+ 3.1.2
+
+ ${gson.isInternalModule}
+
org.apache.maven.plugins
@@ -444,6 +470,8 @@
japicmp-maven-plugin
0.21.1
+ ${gson.isTestModule}
+
${project.groupId}
@@ -485,6 +513,8 @@
check
+ ${gson.isTestModule}
+
-
4.0.0
com.google.code.gson
@@ -33,6 +32,9 @@
2024-05-19T18:54:10Z
4.26.1
+
+
+ true
@@ -102,19 +104,6 @@
-
-
-
-
- org.apache.maven.plugins
- maven-deploy-plugin
-
-
- true
-
-
-
-
diff --git a/shrinker-test/pom.xml b/shrinker-test/pom.xml
index 293fb059a6..10ec6f1f1c 100644
--- a/shrinker-test/pom.xml
+++ b/shrinker-test/pom.xml
@@ -22,15 +22,14 @@
gson-parent
2.11.1-SNAPSHOT
-
shrinker-test
+ Test: Code shrinking (ProGuard / R8)
-
-
- 2024-05-19T18:54:10Z
-
8
+
+
+ true
@@ -61,35 +60,6 @@
-
-
-
- com.github.siom79.japicmp
- japicmp-maven-plugin
-
-
- true
-
-
-
- org.codehaus.mojo
- animal-sniffer-maven-plugin
-
-
- true
-
-
-
- org.apache.maven.plugins
- maven-deploy-plugin
-
-
- true
-
-
-
-
-