diff --git a/README.md b/README.md
index f5eab3fe26..189ce64ba2 100644
--- a/README.md
+++ b/README.md
@@ -70,6 +70,7 @@ see [`GsonBuilder.disableJdkUnsafe()`](https://javadoc.io/doc/com.google.code.gs
#### Minimum Android API level
+- Gson 2.14.0 and newer: API level 23
- Gson 2.11.0 and newer: API level 21
- Gson 2.10.1 and older: API level 19
diff --git a/gson/pom.xml b/gson/pom.xml
index 0d03552ce0..52ade87bfd 100644
--- a/gson/pom.xml
+++ b/gson/pom.xml
@@ -39,6 +39,7 @@
2025-09-10T20:39:14Z**/Java17*
+ false
@@ -179,6 +180,23 @@
configuration locally). -->
--illegal-access=deny
+
+
+
+ java-time-test
+
+ test
+
+ test
+
+ ${skipJavaTimeTest}
+ --add-opens java.base/java.time=ALL-UNNAMED
+ JavaTimeTest
+ true
+
+
+
@@ -392,6 +410,7 @@
gson-subset
+ true${project.build.directory}/gson-subset-src
diff --git a/gson/src/main/java/com/google/gson/internal/bind/EnumTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/EnumTypeAdapter.java
index b3dbb680c1..8205f26d97 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/EnumTypeAdapter.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/EnumTypeAdapter.java
@@ -51,7 +51,10 @@ public TypeAdapter create(Gson gson, TypeToken typeToken) {
};
/**
- * Taken from Java 19 method {@link HashMap.newHashMap}, using default load factor {@code 0.75F}.
+ * Calculates the 'capacity' needed to hold {@code numMappings} entries without resizing, using
+ * default load factor {@code 0.75F}.
+ *
+ *
Taken from Java 19 method {@link HashMap#newHashMap}.
*/
private static int calculateHashMapCapacity(int numMappings) {
return (int) Math.ceil(numMappings / 0.75F);
diff --git a/gson/src/main/java/com/google/gson/internal/bind/IgnoreJRERequirement.java b/gson/src/main/java/com/google/gson/internal/bind/IgnoreJRERequirement.java
index c51a034867..5cc216ba9e 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/IgnoreJRERequirement.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/IgnoreJRERequirement.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2026 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.internal.bind;
import java.lang.annotation.ElementType;
@@ -5,6 +20,10 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+/**
+ * Used for animal-sniffer-maven-plugin to suppress warnings about API being unavailable for the
+ * target Android API Level.
+ */
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE, ElementType.FIELD})
@SuppressWarnings("IdentifierName")
diff --git a/gson/src/main/java/com/google/gson/internal/bind/JavaTimeTypeAdapters.java b/gson/src/main/java/com/google/gson/internal/bind/JavaTimeTypeAdapters.java
index 791f15b44a..5823d30ffd 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/JavaTimeTypeAdapters.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/JavaTimeTypeAdapters.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2026 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.internal.bind;
import static java.lang.Math.toIntExact;
@@ -9,7 +24,6 @@
import com.google.gson.internal.bind.TypeAdapters.IntegerFieldsTypeAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
-import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.time.Duration;
@@ -39,8 +53,11 @@
* is obviously fragile, and it also needs special {@code --add-opens} configuration with more
* recent JDK versions. So here we freeze the representation that was current with JDK 21, in a way
* that does not use reflection.
+ *
+ *
This class should not directly be used, instead the type adapter factory should be obtained
+ * from {@link TypeAdapters#javaTimeTypeAdapterFactory()}.
*/
-@IgnoreJRERequirement // Protected by a reflective check
+@IgnoreJRERequirement // Protected by a reflective check in `TypeAdapters`
final class JavaTimeTypeAdapters implements TypeAdapters.FactorySupplier {
@Override
@@ -91,7 +108,7 @@ long[] integerValues(LocalDate localDate) {
}
};
- public static final TypeAdapter LOCAL_TIME =
+ private static final TypeAdapter LOCAL_TIME =
new IntegerFieldsTypeAdapter("hour", "minute", "second", "nano") {
@Override
LocalTime create(long[] values) {
@@ -119,7 +136,7 @@ public LocalDateTime read(JsonReader in) throws IOException {
LocalDate localDate = null;
LocalTime localTime = null;
in.beginObject();
- while (in.peek() != JsonToken.END_OBJECT) {
+ while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "date":
@@ -172,7 +189,7 @@ public OffsetDateTime read(JsonReader in) throws IOException {
in.beginObject();
LocalDateTime localDateTime = null;
ZoneOffset zoneOffset = null;
- while (in.peek() != JsonToken.END_OBJECT) {
+ while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "dateTime":
@@ -213,7 +230,7 @@ public OffsetTime read(JsonReader in) throws IOException {
in.beginObject();
LocalTime localTime = null;
ZoneOffset zoneOffset = null;
- while (in.peek() != JsonToken.END_OBJECT) {
+ while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "time":
@@ -287,7 +304,7 @@ long[] integerValues(YearMonth yearMonth) {
// A ZoneId is either a ZoneOffset or a ZoneRegion, where ZoneOffset is public and ZoneRegion is
// not. For compatibility with reflection-based serialization, we need to write the "id" field of
// ZoneRegion if we have a ZoneRegion, and we need to write the "totalSeconds" field of ZoneOffset
- // if we have a ZoneOffset. When reading, we need to construct the the appropriate thing depending
+ // if we have a ZoneOffset. When reading, we need to construct the appropriate thing depending
// on which of those two fields we see.
private static final TypeAdapter ZONE_ID =
new TypeAdapter() {
@@ -296,7 +313,7 @@ public ZoneId read(JsonReader in) throws IOException {
in.beginObject();
String id = null;
Integer totalSeconds = null;
- while (in.peek() != JsonToken.END_OBJECT) {
+ while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "id":
@@ -348,7 +365,7 @@ public ZonedDateTime read(JsonReader in) throws IOException {
LocalDateTime localDateTime = null;
ZoneOffset zoneOffset = null;
ZoneId zoneId = null;
- while (in.peek() != JsonToken.END_OBJECT) {
+ while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "dateTime":
@@ -390,7 +407,7 @@ public void write(JsonWriter out, ZonedDateTime value) throws IOException {
}.nullSafe();
}
- static final TypeAdapterFactory JAVA_TIME_FACTORY =
+ private static final TypeAdapterFactory JAVA_TIME_FACTORY =
new TypeAdapterFactory() {
@Override
public TypeAdapter create(Gson gson, TypeToken typeToken) {
diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java
index 852ae56c5b..ee467d0c1e 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java
@@ -807,6 +807,8 @@ public void write(JsonWriter out, Currency value) throws IOException {
* An abstract {@link TypeAdapter} for classes whose JSON serialization consists of a fixed set of
* integer fields. That is the case for {@link Calendar} and the legacy serialization of various
* {@code java.time} types.
+ *
+ *
This class handles {@code null}; subclasses don't have to use {@link #nullSafe()}.
*/
abstract static class IntegerFieldsTypeAdapter extends TypeAdapter {
private final List fields;
@@ -815,8 +817,21 @@ abstract static class IntegerFieldsTypeAdapter extends TypeAdapter {
this.fields = Arrays.asList(fields);
}
+ /**
+ * On deserialization: Creates an object from the integer values. Subclasses should use {@link
+ * Math#toIntExact(long)} and similar if necessary to prevent silent truncation.
+ *
+ *
Values have the same order as the field names provided to the {@linkplain
+ * #IntegerFieldsTypeAdapter(String[]) constructor}.
+ */
abstract T create(long[] values);
+ /**
+ * On serialization: Extracts the integer values from the object.
+ *
+ *
Values must have the same order as the field names provided to the {@linkplain
+ * #IntegerFieldsTypeAdapter(String[]) constructor}.
+ */
abstract long[] integerValues(T t);
@Override
@@ -827,7 +842,7 @@ public T read(JsonReader in) throws IOException {
}
in.beginObject();
long[] values = new long[fields.size()];
- while (in.peek() != JsonToken.END_OBJECT) {
+ while (in.hasNext()) {
String name = in.nextName();
int index = fields.indexOf(name);
if (index >= 0) {
@@ -884,7 +899,7 @@ long[] integerValues(Calendar calendar) {
}
};
- // TODO: update this when we are on at least Android API Level 24.
+ // TODO: switch to `Math#toIntExact` when we are on at least Android API Level 24.
private static int toIntExact(long x) {
int i = (int) x;
if (i != x) {
@@ -946,6 +961,10 @@ interface FactorySupplier {
TypeAdapterFactory get();
}
+ /**
+ * Adapter factory for {@code java.time} classes. Returns {@code null} if not supported by the
+ * current environment (e.g. too old Android version, without desugaring).
+ */
public static TypeAdapterFactory javaTimeTypeAdapterFactory() {
try {
Class> javaTimeTypeAdapterFactoryClass =
diff --git a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java
index 6dac057a8e..caa2d61774 100644
--- a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java
+++ b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java
@@ -30,14 +30,11 @@
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
-import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory;
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.Constructor;
-import java.lang.reflect.Field;
-import java.lang.reflect.InaccessibleObjectException;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
@@ -45,20 +42,6 @@
import java.net.URI;
import java.net.URL;
import java.text.DateFormat;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.time.MonthDay;
-import java.time.OffsetDateTime;
-import java.time.OffsetTime;
-import java.time.Period;
-import java.time.Year;
-import java.time.YearMonth;
-import java.time.ZoneId;
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
@@ -78,7 +61,7 @@
import org.junit.Test;
/**
- * Functional test for Json serialization and deserialization for common classes for which default
+ * Functional test for JSON serialization and deserialization for common classes for which default
* support is provided in Gson. The tests for Map types are available in {@link MapTest}.
*
* @author Inderjeet Singh
@@ -217,13 +200,6 @@ public void testNullSerialization() {
testNullSerializationAndDeserialization(GregorianCalendar.class);
testNullSerializationAndDeserialization(Calendar.class);
testNullSerializationAndDeserialization(Class.class);
- testNullSerializationAndDeserialization(Duration.class);
- testNullSerializationAndDeserialization(Instant.class);
- testNullSerializationAndDeserialization(LocalDate.class);
- testNullSerializationAndDeserialization(LocalTime.class);
- testNullSerializationAndDeserialization(LocalDateTime.class);
- testNullSerializationAndDeserialization(ZoneId.class);
- testNullSerializationAndDeserialization(ZonedDateTime.class);
}
private void testNullSerializationAndDeserialization(Class> c) {
@@ -624,7 +600,7 @@ public void testDateDeserializationWithPattern() {
}
@Test
- public void testDateSerializationWithPatternNotOverridenByTypeAdapter() {
+ public void testDateSerializationWithPatternNotOverriddenByTypeAdapter() {
String pattern = "yyyy-MM-dd";
Gson gson =
new GsonBuilder()
@@ -836,232 +812,6 @@ public void testStringBufferDeserialization() {
assertThat(sb.toString()).isEqualTo("abc");
}
- @Test
- public void testJavaTimeDuration() {
- Duration duration = Duration.ofSeconds(123, 456_789_012);
- String json = "{\"seconds\":123,\"nanos\":456789012}";
- roundTrip(duration, json);
- }
-
- @Test
- public void testJavaTimeDurationWithUnknownFields() {
- Duration duration = Duration.ofSeconds(123, 456_789_012);
- String json = "{\"seconds\":123,\"nanos\":456789012,\"tiddly\":\"pom\",\"wibble\":\"wobble\"}";
- assertThat(gson.fromJson(json, Duration.class)).isEqualTo(duration);
- }
-
- @Test
- public void testJavaTimeInstant() {
- Instant instant = Instant.ofEpochSecond(123, 456_789_012);
- String json = "{\"seconds\":123,\"nanos\":456789012}";
- roundTrip(instant, json);
- }
-
- @Test
- public void testJavaTimeLocalDate() {
- LocalDate localDate = LocalDate.of(2021, 12, 2);
- String json = "{\"year\":2021,\"month\":12,\"day\":2}";
- roundTrip(localDate, json);
- }
-
- @Test
- public void testJavaTimeLocalTime() {
- LocalTime localTime = LocalTime.of(12, 34, 56, 789_012_345);
- String json = "{\"hour\":12,\"minute\":34,\"second\":56,\"nano\":789012345}";
- roundTrip(localTime, json);
- }
-
- @Test
- public void testJavaTimeLocalDateTime() {
- LocalDateTime localDateTime = LocalDateTime.of(2021, 12, 2, 12, 34, 56, 789_012_345);
- String json =
- "{\"date\":{\"year\":2021,\"month\":12,\"day\":2},"
- + "\"time\":{\"hour\":12,\"minute\":34,\"second\":56,\"nano\":789012345}}";
- roundTrip(localDateTime, json);
- }
-
- @Test
- public void testJavaTimeMonthDay() {
- MonthDay monthDay = MonthDay.of(2, 17);
- String json = "{\"month\":2,\"day\":17}";
- roundTrip(monthDay, json);
- }
-
- @Test
- public void testJavaTimeOffsetDateTime() {
- OffsetDateTime offsetDateTime =
- OffsetDateTime.of(
- LocalDate.of(2021, 12, 2), LocalTime.of(12, 34, 56, 789_012_345), ZoneOffset.UTC);
- String json =
- "{\"dateTime\":{\"date\":{\"year\":2021,\"month\":12,\"day\":2},"
- + "\"time\":{\"hour\":12,\"minute\":34,\"second\":56,\"nano\":789012345}},"
- + "\"offset\":{\"totalSeconds\":0}}";
- roundTrip(offsetDateTime, json);
- }
-
- @Test
- public void testJavaTimeOffsetTime() {
- OffsetTime offsetTime = OffsetTime.of(LocalTime.of(12, 34, 56, 789_012_345), ZoneOffset.UTC);
- String json =
- "{\"time\":{\"hour\":12,\"minute\":34,\"second\":56,\"nano\":789012345},"
- + "\"offset\":{\"totalSeconds\":0}}";
- roundTrip(offsetTime, json);
- }
-
- @Test
- public void testJavaTimePeriod() {
- Period period = Period.of(2025, 2, 3);
- String json = "{\"years\":2025,\"months\":2,\"days\":3}";
- roundTrip(period, json);
- }
-
- @Test
- public void testJavaTimeYear() {
- Year year = Year.of(2025);
- String json = "{\"year\":2025}";
- roundTrip(year, json);
- }
-
- @Test
- public void testJavaTimeYearMonth() {
- YearMonth yearMonth = YearMonth.of(2025, 2);
- String json = "{\"year\":2025,\"month\":2}";
- roundTrip(yearMonth, json);
- }
-
- @Test
- public void testJavaTimeZoneOffset() {
- ZoneOffset zoneOffset = ZoneOffset.ofTotalSeconds(-8 * 60 * 60);
- String json = "{\"totalSeconds\":-28800}";
- roundTrip(zoneOffset, json);
- }
-
- @Test
- public void testJavaTimeZoneRegion() {
- ZoneId zoneId = ZoneId.of("Asia/Shanghai");
- String json = "{\"id\":\"Asia/Shanghai\"}";
- roundTrip(zoneId, ZoneId.class, json);
- }
-
- @Test
- public void testJavaTimeZonedDateTimeWithZoneOffset() {
- ZonedDateTime zonedDateTime =
- ZonedDateTime.of(
- LocalDate.of(2021, 12, 2), LocalTime.of(12, 34, 56, 789_012_345), ZoneOffset.UTC);
- String json =
- "{\"dateTime\":{\"date\":{\"year\":2021,\"month\":12,\"day\":2},"
- + "\"time\":{\"hour\":12,\"minute\":34,\"second\":56,\"nano\":789012345}},"
- + "\"offset\":{\"totalSeconds\":0},"
- + "\"zone\":{\"totalSeconds\":0}}";
- roundTrip(zonedDateTime, json);
- }
-
- @Test
- public void testJavaTimeZonedDateTimeWithZoneId() {
- ZoneId zoneId = ZoneId.of("UTC+01:00");
- int totalSeconds = ((ZoneOffset) zoneId.normalized()).getTotalSeconds();
- ZonedDateTime zonedDateTime =
- ZonedDateTime.of(LocalDate.of(2021, 12, 2), LocalTime.of(12, 34, 56, 789_012_345), zoneId);
- String json =
- "{\"dateTime\":{\"date\":{\"year\":2021,\"month\":12,\"day\":2},"
- + "\"time\":{\"hour\":12,\"minute\":34,\"second\":56,\"nano\":789012345}},"
- + "\"offset\":{\"totalSeconds\":"
- + totalSeconds
- + "},"
- + "\"zone\":{\"id\":\""
- + zoneId.getId()
- + "\"}}";
- roundTrip(zonedDateTime, json);
- }
-
- @Test
- public void testJavaTimeZonedDateTimeWithZoneIdThatHasAdapter() {
- TypeAdapter zoneIdAdapter =
- new TypeAdapter() {
- @Override
- public void write(JsonWriter out, ZoneId value) throws IOException {
- out.value(value.getId());
- }
-
- @Override
- public ZoneId read(JsonReader in) throws IOException {
- return ZoneId.of(in.nextString());
- }
- };
- Gson customGson = new GsonBuilder().registerTypeAdapter(ZoneId.class, zoneIdAdapter).create();
- ZoneId zoneId = ZoneId.of("UTC+01:00");
- int totalSeconds = ((ZoneOffset) zoneId.normalized()).getTotalSeconds();
- ZonedDateTime zonedDateTime =
- ZonedDateTime.of(LocalDate.of(2021, 12, 2), LocalTime.of(12, 34, 56, 789_012_345), zoneId);
- String json =
- "{\"dateTime\":{\"date\":{\"year\":2021,\"month\":12,\"day\":2},"
- + "\"time\":{\"hour\":12,\"minute\":34,\"second\":56,\"nano\":789012345}},"
- + "\"offset\":{\"totalSeconds\":"
- + totalSeconds
- + "},"
- + "\"zone\":\""
- + zoneId.getId()
- + "\"}";
- roundTrip(customGson, zonedDateTime, ZonedDateTime.class, json);
- }
-
- private static final boolean JAVA_TIME_FIELDS_ARE_ACCESSIBLE;
-
- static {
- boolean accessible = false;
- try {
- Instant.class.getDeclaredField("seconds").setAccessible(true);
- accessible = true;
- } catch (InaccessibleObjectException e) {
- // OK: we can't reflect on java.time fields
- } catch (NoSuchFieldException e) {
- // JDK implementation has changed and we no longer have an Instant.seconds field.
- throw new AssertionError(e);
- }
- JAVA_TIME_FIELDS_ARE_ACCESSIBLE = accessible;
- }
-
- private void roundTrip(Object value, String expectedJson) {
- roundTrip(value, value.getClass(), expectedJson);
- }
-
- private void roundTrip(Object value, Class> valueClass, String expectedJson) {
- roundTrip(gson, value, valueClass, expectedJson);
- if (JAVA_TIME_FIELDS_ARE_ACCESSIBLE) {
- checkReflectiveTypeAdapterFactory(value, expectedJson);
- }
- }
-
- private void roundTrip(Gson customGson, Object value, Class> valueClass, String expectedJson) {
- assertThat(customGson.getAdapter(valueClass).getClass().getName()).doesNotContain("Reflective");
- assertThat(customGson.toJson(value, valueClass)).isEqualTo(expectedJson);
- assertThat(customGson.fromJson(expectedJson, valueClass)).isEqualTo(value);
- }
-
- // Assuming we have reflective access to the fields of java.time classes, check that
- // ReflectiveTypeAdapterFactory would produce the same JSON. This ensures that we are preserving
- // a compatible JSON format for those classes even though we no longer use reflection.
- private void checkReflectiveTypeAdapterFactory(Object value, String expectedJson) {
- List> factories;
- try {
- Field factoriesField = gson.getClass().getDeclaredField("factories");
- factoriesField.setAccessible(true);
- factories = (List>) factoriesField.get(gson);
- } catch (ReflectiveOperationException e) {
- throw new LinkageError(e.getMessage(), e);
- }
- ReflectiveTypeAdapterFactory adapterFactory =
- factories.stream()
- .filter(f -> f instanceof ReflectiveTypeAdapterFactory)
- .map(f -> (ReflectiveTypeAdapterFactory) f)
- .findFirst()
- .get();
- TypeToken> typeToken = TypeToken.get(value.getClass());
- @SuppressWarnings("unchecked")
- TypeAdapter