JavaTimeTypeAdapters.java
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.
*
* <p>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> DURATION =
new IntegerFieldsTypeAdapter<Duration>("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> INSTANT =
new IntegerFieldsTypeAdapter<Instant>("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<LocalDate> LOCAL_DATE =
new IntegerFieldsTypeAdapter<LocalDate>("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<LocalTime> LOCAL_TIME =
new IntegerFieldsTypeAdapter<LocalTime>("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> localDateTime(Gson gson) {
TypeAdapter<LocalDate> localDateAdapter = gson.getAdapter(LocalDate.class);
TypeAdapter<LocalTime> localTimeAdapter = gson.getAdapter(LocalTime.class);
return new TypeAdapter<LocalDateTime>() {
@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<MonthDay> MONTH_DAY =
new IntegerFieldsTypeAdapter<MonthDay>("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> offsetDateTime(Gson gson) {
TypeAdapter<LocalDateTime> localDateTimeAdapter = localDateTime(gson);
TypeAdapter<ZoneOffset> zoneOffsetAdapter = gson.getAdapter(ZoneOffset.class);
return new TypeAdapter<OffsetDateTime>() {
@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> offsetTime(Gson gson) {
TypeAdapter<LocalTime> localTimeAdapter = gson.getAdapter(LocalTime.class);
TypeAdapter<ZoneOffset> zoneOffsetAdapter = gson.getAdapter(ZoneOffset.class);
return new TypeAdapter<OffsetTime>() {
@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> PERIOD =
new IntegerFieldsTypeAdapter<Period>("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> YEAR =
new IntegerFieldsTypeAdapter<Year>("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<YearMonth> YEAR_MONTH =
new IntegerFieldsTypeAdapter<YearMonth>("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 the appropriate thing depending
// on which of those two fields we see.
private static final TypeAdapter<ZoneId> ZONE_ID =
new TypeAdapter<ZoneId>() {
@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> zonedDateTime(Gson gson) {
TypeAdapter<LocalDateTime> localDateTimeAdapter = localDateTime(gson);
TypeAdapter<ZoneOffset> zoneOffsetAdapter = gson.getAdapter(ZoneOffset.class);
TypeAdapter<ZoneId> zoneIdAdapter = gson.getAdapter(ZoneId.class);
return new TypeAdapter<ZonedDateTime>() {
@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 <T> TypeAdapter<T> create(Gson gson, TypeToken<T> 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<T> result = (TypeAdapter<T>) adapter;
return result;
}
};
private static <T> T requireNonNullField(T field, String fieldName, JsonReader reader) {
if (field == null) {
throw new JsonSyntaxException(
"Missing " + fieldName + " field; at path " + reader.getPreviousPath());
}
return field;
}
}