diff --git a/ReleaseProcess.md b/ReleaseProcess.md
index f431ea621a..cae620ffeb 100644
--- a/ReleaseProcess.md
+++ b/ReleaseProcess.md
@@ -34,7 +34,7 @@ The following describes how to perform the steps of the release locally to verif
> [!CAUTION]\
> Be careful with this, these steps might be outdated or incomplete. Double-check that you are working on a copy of your local Gson Git repository and make sure you have followed all steps. To be safe you can also temporarily turn off your internet connection to avoid accidentally pushing changes to the real remote Git or Maven repository.\
-> As an alternative to the steps described below you can instead [perform a dry run](https://maven.apache.org/maven-release/maven-release-plugin/usage.html#do-a-dry-run), though this might not behave identical to a real release.
+> As an alternative to the steps described below you can instead [perform a dry run](https://maven.apache.org/maven-release/maven-release-plugin/usage.html#do-a-dry-run), though this might not behave identically to a real release.
1. Make a copy of your local Gson Git repository and only work with that copy
1. Make sure you are on the `main` branch
diff --git a/gson/pom.xml b/gson/pom.xml
index c133ee45bf..19baf84984 100644
--- a/gson/pom.xml
+++ b/gson/pom.xml
@@ -39,6 +39,7 @@
2026-10-24T01:07:07Z**/Java17*
+ false
@@ -185,6 +186,23 @@
configuration locally). -->
--illegal-access=deny
+
+
+
+ java-time-test
+
+ test
+
+ test
+
+ ${skipJavaTimeTest}
+ --add-opens java.base/java.time=ALL-UNNAMED
+ JavaTimeTest
+ true
+
+
+
@@ -402,6 +420,7 @@
gson-subset
+ true${project.build.directory}/gson-subset-src
diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java
index 64b076ede4..d9b1f13654 100644
--- a/gson/src/main/java/com/google/gson/GsonBuilder.java
+++ b/gson/src/main/java/com/google/gson/GsonBuilder.java
@@ -977,10 +977,7 @@ List createFactories(
factories.add(TypeAdapters.BIT_SET_FACTORY);
factories.add(DefaultDateTypeAdapter.DEFAULT_STYLE_FACTORY);
factories.add(TypeAdapters.CALENDAR_FACTORY);
- TypeAdapterFactory javaTimeFactory = TypeAdapters.javaTimeTypeAdapterFactory();
- if (javaTimeFactory != null) {
- factories.add(javaTimeFactory);
- }
+ factories.add(TypeAdapters.JAVA_TIME_FACTORY);
factories.add(TypeAdapters.JDK_OPTIONAL_FACTORY);
factories.addAll(SqlTypesSupport.SQL_TYPE_FACTORIES);
factories.add(ArrayTypeAdapter.FACTORY);
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..c0c51e5ae9 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 {@code 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/IgnoreAndroidApiLevelRequirement.java b/gson/src/main/java/com/google/gson/internal/bind/IgnoreAndroidApiLevelRequirement.java
new file mode 100644
index 0000000000..55cae77fda
--- /dev/null
+++ b/gson/src/main/java/com/google/gson/internal/bind/IgnoreAndroidApiLevelRequirement.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+import java.lang.annotation.Retention;
+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")
+@interface IgnoreAndroidApiLevelRequirement {}
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
deleted file mode 100644
index c51a034867..0000000000
--- a/gson/src/main/java/com/google/gson/internal/bind/IgnoreJRERequirement.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.google.gson.internal.bind;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-@Retention(RetentionPolicy.CLASS)
-@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE, ElementType.FIELD})
-@SuppressWarnings("IdentifierName")
-@interface IgnoreJRERequirement {}
diff --git a/gson/src/main/java/com/google/gson/internal/bind/JavaTimeTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/JavaTimeTypeAdapterFactory.java
new file mode 100644
index 0000000000..6305760242
--- /dev/null
+++ b/gson/src/main/java/com/google/gson/internal/bind/JavaTimeTypeAdapterFactory.java
@@ -0,0 +1,491 @@
+/*
+ * 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;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+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.JsonWriter;
+import java.io.IOException;
+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;
+
+/**
+ * Type adapter factory for {@code java.time} types.
+ *
+ *
These adapters mimic what {@link ReflectiveTypeAdapterFactory} would produce for the same
+ * types. That is by no means a natural encoding, given that many of the types have standard ISO
+ * representations. If Gson had added support for the types at the same time they appeared (in Java
+ * 8, released in 2014), it would surely have used those representations. Unfortunately, in the
+ * intervening time, people have been using the reflective representations, and changing that would
+ * potentially be incompatible. Meanwhile, depending on the details of private fields in JDK classes
+ * 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.
+ */
+final class JavaTimeTypeAdapterFactory implements TypeAdapterFactory {
+
+ @Override
+ public TypeAdapter create(Gson gson, TypeToken type) {
+ Class> rawType = type.getRawType();
+ String className = rawType.getName();
+ // Only try to load `java.time` classes when handling a `java.time` type, otherwise this could
+ // lead to a LinkageError on Android if `java.time` is unavailable
+ // Additionally handle "j$.time", the package name used by Android API desugaring, because Gson
+ // code is rewritten by it as well so all `java.time` references below will then actually refer
+ // to `j$.time` types
+ // It seems even with R8 shrinking and obfuscation the `j$.time` name is preserved
+ if (className.startsWith("java.time.") || className.startsWith("j$.time.")) {
+ return createImpl(gson, rawType);
+ }
+ return null;
+ }
+
+ // Separate method to really only load the `Impl` class when dealing with `java.time` types,
+ // avoiding LinkageError when `java.time` is not available
+ private TypeAdapter createImpl(Gson gson, Class> rawType) {
+ return Impl.createAdapter(gson, rawType);
+ }
+
+ /**
+ * Contains the actual implementation, and all access to {@code java.time} types.
+ *
+ *
On Android support for {@code java.time} was only added in API Level 26, but Gson currently
+ * targets a lower API Level as minimum. Therefore this class is only accessed when actually
+ * handling a {@code java.time} type, meaning the application runs on a JDK, on Android API Level
+ * >= 26, or it uses Android API
+ * desugaring.
+ *
+ *
In case of Android API desugaring, D8 includes its own implementation of {@code java.time}
+ * and rewrites all references to it in the app. This also affects the Gson code (included as
+ * dependency) where all {@code java.time} references are rewritten to {@code j$.time}.
+ */
+ @IgnoreAndroidApiLevelRequirement // access to `java.time`; guarded by package name check above
+ private static class Impl {
+ private static final TypeAdapter DURATION =
+ new IntegerFieldsTypeAdapter("seconds", "nanos") {
+ @Override
+ Duration create(long[] values) {
+ return Duration.ofSeconds(values[0], values[1]);
+ }
+
+ @Override
+ @SuppressWarnings("JavaDurationGetSecondsGetNano")
+ long[] integerValues(Duration duration) {
+ return new long[] {duration.getSeconds(), duration.getNano()};
+ }
+ };
+
+ private static final TypeAdapter INSTANT =
+ new IntegerFieldsTypeAdapter("seconds", "nanos") {
+ @Override
+ Instant create(long[] values) {
+ return Instant.ofEpochSecond(values[0], values[1]);
+ }
+
+ @Override
+ @SuppressWarnings("JavaInstantGetSecondsGetNano")
+ long[] integerValues(Instant instant) {
+ return new long[] {instant.getEpochSecond(), instant.getNano()};
+ }
+ };
+
+ private static final TypeAdapter LOCAL_DATE =
+ new IntegerFieldsTypeAdapter("year", "month", "day") {
+ @Override
+ LocalDate create(long[] values) {
+ return LocalDate.of(
+ toIntExact(values[0]), toIntExact(values[1]), toIntExact(values[2]));
+ }
+
+ @Override
+ long[] integerValues(LocalDate localDate) {
+ return new long[] {
+ localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth()
+ };
+ }
+ };
+
+ private static final TypeAdapter LOCAL_TIME =
+ new IntegerFieldsTypeAdapter("hour", "minute", "second", "nano") {
+ @Override
+ LocalTime create(long[] values) {
+ return LocalTime.of(
+ toIntExact(values[0]),
+ toIntExact(values[1]),
+ toIntExact(values[2]),
+ toIntExact(values[3]));
+ }
+
+ @Override
+ long[] integerValues(LocalTime localTime) {
+ return new long[] {
+ localTime.getHour(), localTime.getMinute(), localTime.getSecond(), localTime.getNano()
+ };
+ }
+ };
+
+ private static TypeAdapter localDateTime(Gson gson) {
+ TypeAdapter localDateAdapter = gson.getAdapter(LocalDate.class);
+ TypeAdapter localTimeAdapter = gson.getAdapter(LocalTime.class);
+ return new TypeAdapter() {
+ @Override
+ public LocalDateTime read(JsonReader in) throws IOException {
+ LocalDate localDate = null;
+ LocalTime localTime = null;
+ in.beginObject();
+ while (in.hasNext()) {
+ String name = in.nextName();
+ switch (name) {
+ case "date":
+ localDate = localDateAdapter.read(in);
+ break;
+ case "time":
+ localTime = localTimeAdapter.read(in);
+ break;
+ default:
+ // Ignore other fields.
+ in.skipValue();
+ }
+ }
+ in.endObject();
+ return LocalDateTime.of(
+ requireNonNullField(localDate, "date", in),
+ requireNonNullField(localTime, "time", in));
+ }
+
+ @Override
+ public void write(JsonWriter out, LocalDateTime value) throws IOException {
+ out.beginObject();
+ out.name("date");
+ localDateAdapter.write(out, value.toLocalDate());
+ out.name("time");
+ localTimeAdapter.write(out, value.toLocalTime());
+ out.endObject();
+ }
+ }.nullSafe();
+ }
+
+ private static final TypeAdapter MONTH_DAY =
+ new IntegerFieldsTypeAdapter("month", "day") {
+ @Override
+ MonthDay create(long[] values) {
+ return MonthDay.of(toIntExact(values[0]), toIntExact(values[1]));
+ }
+
+ @Override
+ long[] integerValues(MonthDay monthDay) {
+ return new long[] {monthDay.getMonthValue(), monthDay.getDayOfMonth()};
+ }
+ };
+
+ private static TypeAdapter offsetDateTime(Gson gson) {
+ TypeAdapter localDateTimeAdapter = localDateTime(gson);
+ TypeAdapter zoneOffsetAdapter = gson.getAdapter(ZoneOffset.class);
+ return new TypeAdapter() {
+ @Override
+ public OffsetDateTime read(JsonReader in) throws IOException {
+ in.beginObject();
+ LocalDateTime localDateTime = null;
+ ZoneOffset zoneOffset = null;
+ while (in.hasNext()) {
+ String name = in.nextName();
+ switch (name) {
+ case "dateTime":
+ localDateTime = localDateTimeAdapter.read(in);
+ break;
+ case "offset":
+ zoneOffset = zoneOffsetAdapter.read(in);
+ break;
+ default:
+ // Ignore other fields.
+ in.skipValue();
+ }
+ }
+ in.endObject();
+ return OffsetDateTime.of(
+ requireNonNullField(localDateTime, "dateTime", in),
+ requireNonNullField(zoneOffset, "offset", in));
+ }
+
+ @Override
+ public void write(JsonWriter out, OffsetDateTime value) throws IOException {
+ out.beginObject();
+ out.name("dateTime");
+ localDateTimeAdapter.write(out, value.toLocalDateTime());
+ out.name("offset");
+ zoneOffsetAdapter.write(out, value.getOffset());
+ out.endObject();
+ }
+ }.nullSafe();
+ }
+
+ private static TypeAdapter offsetTime(Gson gson) {
+ TypeAdapter localTimeAdapter = gson.getAdapter(LocalTime.class);
+ TypeAdapter zoneOffsetAdapter = gson.getAdapter(ZoneOffset.class);
+ return new TypeAdapter() {
+ @Override
+ public OffsetTime read(JsonReader in) throws IOException {
+ in.beginObject();
+ LocalTime localTime = null;
+ ZoneOffset zoneOffset = null;
+ while (in.hasNext()) {
+ String name = in.nextName();
+ switch (name) {
+ case "time":
+ localTime = localTimeAdapter.read(in);
+ break;
+ case "offset":
+ zoneOffset = zoneOffsetAdapter.read(in);
+ break;
+ default:
+ // Ignore other fields.
+ in.skipValue();
+ }
+ }
+ in.endObject();
+ return OffsetTime.of(
+ requireNonNullField(localTime, "time", in),
+ requireNonNullField(zoneOffset, "offset", in));
+ }
+
+ @Override
+ public void write(JsonWriter out, OffsetTime value) throws IOException {
+ out.beginObject();
+ out.name("time");
+ localTimeAdapter.write(out, value.toLocalTime());
+ out.name("offset");
+ zoneOffsetAdapter.write(out, value.getOffset());
+ out.endObject();
+ }
+ }.nullSafe();
+ }
+
+ private static final TypeAdapter PERIOD =
+ new IntegerFieldsTypeAdapter("years", "months", "days") {
+ @Override
+ Period create(long[] values) {
+ return Period.of(toIntExact(values[0]), toIntExact(values[1]), toIntExact(values[2]));
+ }
+
+ @Override
+ long[] integerValues(Period period) {
+ return new long[] {period.getYears(), period.getMonths(), period.getDays()};
+ }
+ };
+
+ private static final TypeAdapter YEAR =
+ new IntegerFieldsTypeAdapter("year") {
+ @Override
+ Year create(long[] values) {
+ return Year.of(toIntExact(values[0]));
+ }
+
+ @Override
+ long[] integerValues(Year year) {
+ return new long[] {year.getValue()};
+ }
+ };
+
+ private static final TypeAdapter YEAR_MONTH =
+ new IntegerFieldsTypeAdapter("year", "month") {
+ @Override
+ YearMonth create(long[] values) {
+ return YearMonth.of(toIntExact(values[0]), toIntExact(values[1]));
+ }
+
+ @Override
+ long[] integerValues(YearMonth yearMonth) {
+ return new long[] {yearMonth.getYear(), yearMonth.getMonthValue()};
+ }
+ };
+
+ // 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 appropriate thing
+ // depending on which of those two fields we see.
+ private static final TypeAdapter ZONE_ID =
+ new TypeAdapter() {
+ @Override
+ public ZoneId read(JsonReader in) throws IOException {
+ in.beginObject();
+ String id = null;
+ Integer totalSeconds = null;
+ while (in.hasNext()) {
+ String name = in.nextName();
+ switch (name) {
+ case "id":
+ id = in.nextString();
+ break;
+ case "totalSeconds":
+ totalSeconds = in.nextInt();
+ break;
+ default:
+ // Ignore other fields.
+ in.skipValue();
+ }
+ }
+ in.endObject();
+ if (id != null) {
+ return ZoneId.of(id);
+ } else if (totalSeconds != null) {
+ return ZoneOffset.ofTotalSeconds(totalSeconds);
+ } else {
+ throw new JsonSyntaxException(
+ "Missing id or totalSeconds field; at path " + in.getPreviousPath());
+ }
+ }
+
+ @Override
+ public void write(JsonWriter out, ZoneId value) throws IOException {
+ if (value instanceof ZoneOffset) {
+ out.beginObject();
+ out.name("totalSeconds");
+ out.value(((ZoneOffset) value).getTotalSeconds());
+ out.endObject();
+ } else {
+ out.beginObject();
+ out.name("id");
+ out.value(value.getId());
+ out.endObject();
+ }
+ }
+ }.nullSafe();
+
+ private static TypeAdapter zonedDateTime(Gson gson) {
+ TypeAdapter localDateTimeAdapter = localDateTime(gson);
+ TypeAdapter zoneOffsetAdapter = gson.getAdapter(ZoneOffset.class);
+ TypeAdapter zoneIdAdapter = gson.getAdapter(ZoneId.class);
+ return new TypeAdapter() {
+ @Override
+ public ZonedDateTime read(JsonReader in) throws IOException {
+ in.beginObject();
+ LocalDateTime localDateTime = null;
+ ZoneOffset zoneOffset = null;
+ ZoneId zoneId = null;
+ while (in.hasNext()) {
+ String name = in.nextName();
+ switch (name) {
+ case "dateTime":
+ localDateTime = localDateTimeAdapter.read(in);
+ break;
+ case "offset":
+ zoneOffset = zoneOffsetAdapter.read(in);
+ break;
+ case "zone":
+ zoneId = zoneIdAdapter.read(in);
+ break;
+ default:
+ // Ignore other fields.
+ in.skipValue();
+ }
+ }
+ in.endObject();
+ return ZonedDateTime.ofInstant(
+ requireNonNullField(localDateTime, "dateTime", in),
+ requireNonNullField(zoneOffset, "offset", in),
+ requireNonNullField(zoneId, "zone", in));
+ }
+
+ @Override
+ public void write(JsonWriter out, ZonedDateTime value) throws IOException {
+ if (value == null) {
+ out.nullValue();
+ return;
+ }
+ out.beginObject();
+ out.name("dateTime");
+ localDateTimeAdapter.write(out, value.toLocalDateTime());
+ out.name("offset");
+ zoneOffsetAdapter.write(out, value.getOffset());
+ out.name("zone");
+ zoneIdAdapter.write(out, value.getZone());
+ out.endObject();
+ }
+ }.nullSafe();
+ }
+
+ private static T requireNonNullField(T field, String fieldName, JsonReader reader) {
+ if (field == null) {
+ throw new JsonSyntaxException(
+ "Missing " + fieldName + " field; at path " + reader.getPreviousPath());
+ }
+ return field;
+ }
+
+ /** Creates the adapter, or {@code null} if no adapter exists for the type. */
+ static TypeAdapter createAdapter(Gson gson, Class> rawType) {
+ TypeAdapter> adapter = null;
+ if (rawType == Duration.class) {
+ adapter = DURATION;
+ } else if (rawType == Instant.class) {
+ adapter = INSTANT;
+ } else if (rawType == LocalDate.class) {
+ adapter = LOCAL_DATE;
+ } else if (rawType == LocalTime.class) {
+ adapter = LOCAL_TIME;
+ } else if (rawType == LocalDateTime.class) {
+ adapter = localDateTime(gson);
+ } else if (rawType == MonthDay.class) {
+ adapter = MONTH_DAY;
+ } else if (rawType == OffsetDateTime.class) {
+ adapter = offsetDateTime(gson);
+ } else if (rawType == OffsetTime.class) {
+ adapter = offsetTime(gson);
+ } else if (rawType == Period.class) {
+ adapter = PERIOD;
+ } else if (rawType == Year.class) {
+ adapter = YEAR;
+ } else if (rawType == YearMonth.class) {
+ adapter = YEAR_MONTH;
+ } else if (rawType == ZoneId.class || rawType == ZoneOffset.class) {
+ // We don't check ZoneId.class.isAssignableFrom(rawType) because we don't want to match the
+ // non-public class ZoneRegion in the runtime type check in
+ // TypeAdapterRuntimeTypeWrapper.write. If we did, then our ZONE_ID would take precedence
+ // over a ZoneId adapter that the user might have registered. (This exact situation showed
+ // up in a Google-internal test.)
+ // TODO: Maybe this needs an additional `ZoneId.class.isAssignableFrom(rawType)` check
+ // nonetheless, see https://github.com/google/gson/pull/2972#discussion_r2702021300
+ adapter = ZONE_ID;
+ } else if (rawType == ZonedDateTime.class) {
+ adapter = zonedDateTime(gson);
+ }
+ @SuppressWarnings("unchecked")
+ TypeAdapter result = (TypeAdapter) adapter;
+ return result;
+ }
+ }
+}
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
deleted file mode 100644
index 92fc764765..0000000000
--- a/gson/src/main/java/com/google/gson/internal/bind/JavaTimeTypeAdapters.java
+++ /dev/null
@@ -1,449 +0,0 @@
-package com.google.gson.internal.bind;
-
-import static java.lang.Math.toIntExact;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonSyntaxException;
-import com.google.gson.TypeAdapter;
-import com.google.gson.TypeAdapterFactory;
-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;
-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;
-
-/**
- * Type adapters for {@code java.time} types.
- *
- *
These adapters mimic what {@link ReflectiveTypeAdapterFactory} would produce for the same
- * types. That is by no means a natural encoding, given that many of the types have standard ISO
- * representations. If Gson had added support for the types at the same time they appeared (in Java
- * 8, released in 2014), it would surely have used those representations. Unfortunately, in the
- * intervening time, people have been using the reflective representations, and changing that would
- * potentially be incompatible. Meanwhile, depending on the details of private fields in JDK classes
- * 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.
- */
-@IgnoreJRERequirement // Protected by a reflective check
-final class JavaTimeTypeAdapters implements TypeAdapters.FactorySupplier {
-
- @Override
- public TypeAdapterFactory get() {
- return JAVA_TIME_FACTORY;
- }
-
- private static final TypeAdapter DURATION =
- new IntegerFieldsTypeAdapter("seconds", "nanos") {
- @Override
- Duration create(long[] values) {
- return Duration.ofSeconds(values[0], values[1]);
- }
-
- @Override
- @SuppressWarnings("JavaDurationGetSecondsGetNano")
- long[] integerValues(Duration duration) {
- return new long[] {duration.getSeconds(), duration.getNano()};
- }
- };
-
- private static final TypeAdapter INSTANT =
- new IntegerFieldsTypeAdapter("seconds", "nanos") {
- @Override
- Instant create(long[] values) {
- return Instant.ofEpochSecond(values[0], values[1]);
- }
-
- @Override
- @SuppressWarnings("JavaInstantGetSecondsGetNano")
- long[] integerValues(Instant instant) {
- return new long[] {instant.getEpochSecond(), instant.getNano()};
- }
- };
-
- private static final TypeAdapter LOCAL_DATE =
- new IntegerFieldsTypeAdapter("year", "month", "day") {
- @Override
- LocalDate create(long[] values) {
- return LocalDate.of(toIntExact(values[0]), toIntExact(values[1]), toIntExact(values[2]));
- }
-
- @Override
- long[] integerValues(LocalDate localDate) {
- return new long[] {
- localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth()
- };
- }
- };
-
- public static final TypeAdapter LOCAL_TIME =
- new IntegerFieldsTypeAdapter("hour", "minute", "second", "nano") {
- @Override
- LocalTime create(long[] values) {
- return LocalTime.of(
- toIntExact(values[0]),
- toIntExact(values[1]),
- toIntExact(values[2]),
- toIntExact(values[3]));
- }
-
- @Override
- long[] integerValues(LocalTime localTime) {
- return new long[] {
- localTime.getHour(), localTime.getMinute(), localTime.getSecond(), localTime.getNano()
- };
- }
- };
-
- private static TypeAdapter localDateTime(Gson gson) {
- TypeAdapter localDateAdapter = gson.getAdapter(LocalDate.class);
- TypeAdapter localTimeAdapter = gson.getAdapter(LocalTime.class);
- return new TypeAdapter() {
- @Override
- public LocalDateTime read(JsonReader in) throws IOException {
- LocalDate localDate = null;
- LocalTime localTime = null;
- in.beginObject();
- while (in.peek() != JsonToken.END_OBJECT) {
- String name = in.nextName();
- switch (name) {
- case "date":
- localDate = localDateAdapter.read(in);
- break;
- case "time":
- localTime = localTimeAdapter.read(in);
- break;
- default:
- // Ignore other fields.
- in.skipValue();
- }
- }
- in.endObject();
- return LocalDateTime.of(
- requireNonNullField(localDate, "date", in), requireNonNullField(localTime, "time", in));
- }
-
- @Override
- public void write(JsonWriter out, LocalDateTime value) throws IOException {
- out.beginObject();
- out.name("date");
- localDateAdapter.write(out, value.toLocalDate());
- out.name("time");
- localTimeAdapter.write(out, value.toLocalTime());
- out.endObject();
- }
- }.nullSafe();
- }
-
- private static final TypeAdapter MONTH_DAY =
- new IntegerFieldsTypeAdapter("month", "day") {
- @Override
- MonthDay create(long[] values) {
- return MonthDay.of(toIntExact(values[0]), toIntExact(values[1]));
- }
-
- @Override
- long[] integerValues(MonthDay monthDay) {
- return new long[] {monthDay.getMonthValue(), monthDay.getDayOfMonth()};
- }
- };
-
- private static TypeAdapter offsetDateTime(Gson gson) {
- TypeAdapter localDateTimeAdapter = localDateTime(gson);
- TypeAdapter zoneOffsetAdapter = gson.getAdapter(ZoneOffset.class);
- return new TypeAdapter() {
- @Override
- public OffsetDateTime read(JsonReader in) throws IOException {
- in.beginObject();
- LocalDateTime localDateTime = null;
- ZoneOffset zoneOffset = null;
- while (in.peek() != JsonToken.END_OBJECT) {
- String name = in.nextName();
- switch (name) {
- case "dateTime":
- localDateTime = localDateTimeAdapter.read(in);
- break;
- case "offset":
- zoneOffset = zoneOffsetAdapter.read(in);
- break;
- default:
- // Ignore other fields.
- in.skipValue();
- }
- }
- in.endObject();
- return OffsetDateTime.of(
- requireNonNullField(localDateTime, "dateTime", in),
- requireNonNullField(zoneOffset, "offset", in));
- }
-
- @Override
- public void write(JsonWriter out, OffsetDateTime value) throws IOException {
- out.beginObject();
- out.name("dateTime");
- localDateTimeAdapter.write(out, value.toLocalDateTime());
- out.name("offset");
- zoneOffsetAdapter.write(out, value.getOffset());
- out.endObject();
- }
- }.nullSafe();
- }
-
- private static TypeAdapter offsetTime(Gson gson) {
- TypeAdapter localTimeAdapter = gson.getAdapter(LocalTime.class);
- TypeAdapter zoneOffsetAdapter = gson.getAdapter(ZoneOffset.class);
- return new TypeAdapter() {
- @Override
- public OffsetTime read(JsonReader in) throws IOException {
- in.beginObject();
- LocalTime localTime = null;
- ZoneOffset zoneOffset = null;
- while (in.peek() != JsonToken.END_OBJECT) {
- String name = in.nextName();
- switch (name) {
- case "time":
- localTime = localTimeAdapter.read(in);
- break;
- case "offset":
- zoneOffset = zoneOffsetAdapter.read(in);
- break;
- default:
- // Ignore other fields.
- in.skipValue();
- }
- }
- in.endObject();
- return OffsetTime.of(
- requireNonNullField(localTime, "time", in),
- requireNonNullField(zoneOffset, "offset", in));
- }
-
- @Override
- public void write(JsonWriter out, OffsetTime value) throws IOException {
- out.beginObject();
- out.name("time");
- localTimeAdapter.write(out, value.toLocalTime());
- out.name("offset");
- zoneOffsetAdapter.write(out, value.getOffset());
- out.endObject();
- }
- }.nullSafe();
- }
-
- private static final TypeAdapter PERIOD =
- new IntegerFieldsTypeAdapter("years", "months", "days") {
- @Override
- Period create(long[] values) {
- return Period.of(toIntExact(values[0]), toIntExact(values[1]), toIntExact(values[2]));
- }
-
- @Override
- long[] integerValues(Period period) {
- return new long[] {period.getYears(), period.getMonths(), period.getDays()};
- }
- };
-
- private static final TypeAdapter YEAR =
- new IntegerFieldsTypeAdapter("year") {
- @Override
- Year create(long[] values) {
- return Year.of(toIntExact(values[0]));
- }
-
- @Override
- long[] integerValues(Year year) {
- return new long[] {year.getValue()};
- }
- };
-
- private static final TypeAdapter YEAR_MONTH =
- new IntegerFieldsTypeAdapter("year", "month") {
- @Override
- YearMonth create(long[] values) {
- return YearMonth.of(toIntExact(values[0]), toIntExact(values[1]));
- }
-
- @Override
- long[] integerValues(YearMonth yearMonth) {
- return new long[] {yearMonth.getYear(), yearMonth.getMonthValue()};
- }
- };
-
- // 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 appropriate thing depending
- // on which of those two fields we see.
- private static final TypeAdapter ZONE_ID =
- new TypeAdapter() {
- @Override
- public ZoneId read(JsonReader in) throws IOException {
- in.beginObject();
- String id = null;
- Integer totalSeconds = null;
- while (in.peek() != JsonToken.END_OBJECT) {
- String name = in.nextName();
- switch (name) {
- case "id":
- id = in.nextString();
- break;
- case "totalSeconds":
- totalSeconds = in.nextInt();
- break;
- default:
- // Ignore other fields.
- in.skipValue();
- }
- }
- in.endObject();
- if (id != null) {
- return ZoneId.of(id);
- } else if (totalSeconds != null) {
- return ZoneOffset.ofTotalSeconds(totalSeconds);
- } else {
- throw new JsonSyntaxException(
- "Missing id or totalSeconds field; at path " + in.getPreviousPath());
- }
- }
-
- @Override
- public void write(JsonWriter out, ZoneId value) throws IOException {
- if (value instanceof ZoneOffset) {
- out.beginObject();
- out.name("totalSeconds");
- out.value(((ZoneOffset) value).getTotalSeconds());
- out.endObject();
- } else {
- out.beginObject();
- out.name("id");
- out.value(value.getId());
- out.endObject();
- }
- }
- }.nullSafe();
-
- private static TypeAdapter zonedDateTime(Gson gson) {
- TypeAdapter localDateTimeAdapter = localDateTime(gson);
- TypeAdapter zoneOffsetAdapter = gson.getAdapter(ZoneOffset.class);
- TypeAdapter zoneIdAdapter = gson.getAdapter(ZoneId.class);
- return new TypeAdapter() {
- @Override
- public ZonedDateTime read(JsonReader in) throws IOException {
- in.beginObject();
- LocalDateTime localDateTime = null;
- ZoneOffset zoneOffset = null;
- ZoneId zoneId = null;
- while (in.peek() != JsonToken.END_OBJECT) {
- String name = in.nextName();
- switch (name) {
- case "dateTime":
- localDateTime = localDateTimeAdapter.read(in);
- break;
- case "offset":
- zoneOffset = zoneOffsetAdapter.read(in);
- break;
- case "zone":
- zoneId = zoneIdAdapter.read(in);
- break;
- default:
- // Ignore other fields.
- in.skipValue();
- }
- }
- in.endObject();
- return ZonedDateTime.ofInstant(
- requireNonNullField(localDateTime, "dateTime", in),
- requireNonNullField(zoneOffset, "offset", in),
- requireNonNullField(zoneId, "zone", in));
- }
-
- @Override
- public void write(JsonWriter out, ZonedDateTime value) throws IOException {
- if (value == null) {
- out.nullValue();
- return;
- }
- out.beginObject();
- out.name("dateTime");
- localDateTimeAdapter.write(out, value.toLocalDateTime());
- out.name("offset");
- zoneOffsetAdapter.write(out, value.getOffset());
- out.name("zone");
- zoneIdAdapter.write(out, value.getZone());
- out.endObject();
- }
- }.nullSafe();
- }
-
- static final TypeAdapterFactory JAVA_TIME_FACTORY =
- new TypeAdapterFactory() {
- @Override
- public TypeAdapter create(Gson gson, TypeToken typeToken) {
- Class super T> rawType = typeToken.getRawType();
- if (!rawType.getName().startsWith("java.time.")) {
- // Immediately return null so we don't load all these classes when nobody's doing
- // anything with java.time.
- return null;
- }
- TypeAdapter> adapter = null;
- if (rawType == Duration.class) {
- adapter = DURATION;
- } else if (rawType == Instant.class) {
- adapter = INSTANT;
- } else if (rawType == LocalDate.class) {
- adapter = LOCAL_DATE;
- } else if (rawType == LocalTime.class) {
- adapter = LOCAL_TIME;
- } else if (rawType == LocalDateTime.class) {
- adapter = localDateTime(gson);
- } else if (rawType == MonthDay.class) {
- adapter = MONTH_DAY;
- } else if (rawType == OffsetDateTime.class) {
- adapter = offsetDateTime(gson);
- } else if (rawType == OffsetTime.class) {
- adapter = offsetTime(gson);
- } else if (rawType == Period.class) {
- adapter = PERIOD;
- } else if (rawType == Year.class) {
- adapter = YEAR;
- } else if (rawType == YearMonth.class) {
- adapter = YEAR_MONTH;
- } else if (rawType == ZoneId.class || rawType == ZoneOffset.class) {
- // We don't check ZoneId.class.isAssignableFrom(rawType) because we don't want to match
- // the non-public class ZoneRegion in the runtime type check in
- // TypeAdapterRuntimeTypeWrapper.write. If we did, then our ZONE_ID would take
- // precedence over a ZoneId adapter that the user might have registered. (This exact
- // situation showed up in a Google-internal test.)
- adapter = ZONE_ID;
- } else if (rawType == ZonedDateTime.class) {
- adapter = zonedDateTime(gson);
- }
- @SuppressWarnings("unchecked")
- TypeAdapter result = (TypeAdapter) adapter;
- return result;
- }
- };
-
- private static T requireNonNullField(T field, String fieldName, JsonReader reader) {
- if (field == null) {
- throw new JsonSyntaxException(
- "Missing " + fieldName + " field; at path " + reader.getPreviousPath());
- }
- return field;
- }
-}
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 5ca6875b7b..d20232e135 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
@@ -847,6 +847,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;
@@ -855,8 +857,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
@@ -867,7 +882,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) {
@@ -973,6 +988,8 @@ public void write(JsonWriter out, Locale value) throws IOException {
public static final TypeAdapterFactory ENUM_FACTORY = EnumTypeAdapter.FACTORY;
+ public static final TypeAdapterFactory JAVA_TIME_FACTORY = new JavaTimeTypeAdapterFactory();
+
public static final TypeAdapterFactory JDK_OPTIONAL_FACTORY = JdkOptionalTypeAdapters.FACTORY;
interface FactorySupplier {
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 ca8444f3af..137a982e17 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;
@@ -47,20 +44,6 @@
import java.net.URISyntaxException;
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;
@@ -80,7 +63,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
@@ -244,13 +227,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) {
@@ -863,232 +839,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