From 2e73ca5f6d19026396399638db3cc49ab451287f Mon Sep 17 00:00:00 2001 From: ZhouXY108 Date: Fri, 6 Jun 2025 19:13:52 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(gson):=20=E6=B7=BB=E5=8A=A0=20Gson=20?= =?UTF-8?q?=E9=80=82=E9=85=8D=E5=99=A8=E4=BB=A5=E6=94=AF=E6=8C=81=20Java?= =?UTF-8?q?=208=20=E6=97=A5=E6=9C=9F=E6=97=B6=E9=97=B4=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `LocalDateTimeTypeAdapter`、`LocalDateTypeAdapter` 和 `ZonedDateTimeTypeAdapter` - 将 Gson 作为可选依赖 --- plusone-commons/pom.xml | 13 +- .../gson/LocalDateTimeTypeAdapter.java | 71 ++++++++++ .../commons/gson/LocalDateTypeAdapter.java | 71 ++++++++++ .../gson/ZonedDateTimeTypeAdapter.java | 71 ++++++++++ .../commons/gson/GsonTypeAdapterTests.java | 131 ++++++++++++++++++ 5 files changed, 351 insertions(+), 6 deletions(-) create mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTimeTypeAdapter.java create mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTypeAdapter.java create mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/ZonedDateTimeTypeAdapter.java create mode 100644 plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/GsonTypeAdapterTests.java diff --git a/plusone-commons/pom.xml b/plusone-commons/pom.xml index 223023b..c238688 100644 --- a/plusone-commons/pom.xml +++ b/plusone-commons/pom.xml @@ -55,6 +55,13 @@ true + + + com.google.code.gson + gson + true + + @@ -111,12 +118,6 @@ jackson-datatype-jsr310 test - - - com.google.code.gson - gson - test - diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTimeTypeAdapter.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTimeTypeAdapter.java new file mode 100644 index 0000000..317f404 --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTimeTypeAdapter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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 xyz.zhouxy.plusone.commons.gson; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import xyz.zhouxy.plusone.commons.util.AssertTools; + +/** + * {@code LocalDateTime} 的 {@code TypeAdapter}, + * 用于 Gson 对 {@code LocalDateTime} 进行相互转换。 + * + * @author ZhouXY + * @since 1.1.0 + * @see TypeAdapter + * @see com.google.gson.GsonBuilder + */ +public class LocalDateTimeTypeAdapter extends TypeAdapter { + + private final DateTimeFormatter dateTimeFormatter; + + /** + * 默认构造函数, + * 使用 {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} 进行 {@link LocalDateTime} 的序列化与反序列化。 + */ + public LocalDateTimeTypeAdapter() { + this.dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + } + + /** + * 构造函数, + * 使用传入的 {@link DateTimeFormatter} 进行 {@link LocalDateTime} 的序列化与反序列化。 + * + * @param formatter 用于序列化 {@link LocalDateTime} 的格式化器,不可为 {@code null}。 + */ + public LocalDateTimeTypeAdapter(DateTimeFormatter formatter) { + AssertTools.checkArgumentNotNull(formatter, "formatter can not be null."); + this.dateTimeFormatter = formatter; + } + + /** {@inheritDoc} */ + @Override + public void write(JsonWriter out, LocalDateTime value) throws IOException { + out.value(dateTimeFormatter.format(value)); + } + + /** {@inheritDoc} */ + @Override + public LocalDateTime read(JsonReader in) throws IOException { + return LocalDateTime.parse(in.nextString(), dateTimeFormatter); + } +} diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTypeAdapter.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTypeAdapter.java new file mode 100644 index 0000000..5a42ad3 --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTypeAdapter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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 xyz.zhouxy.plusone.commons.gson; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import xyz.zhouxy.plusone.commons.util.AssertTools; + +/** + * {@code LocalDate} 的 {@code TypeAdapter}, + * 用于 Gson 对 {@code LocalDate} 进行相互转换。 + * + * @author ZhouXY + * @since 1.1.0 + * @see TypeAdapter + * @see com.google.gson.GsonBuilder + */ +public class LocalDateTypeAdapter extends TypeAdapter { + + private final DateTimeFormatter dateTimeFormatter; + + /** + * 默认构造函数, + * 使用 {@link DateTimeFormatter#ISO_LOCAL_DATE} 进行 {@link LocalDate} 的序列化与反序列化。 + */ + public LocalDateTypeAdapter() { + this.dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE; + } + + /** + * 构造函数, + * 使用传入的 {@link DateTimeFormatter} 进行 {@link LocalDate} 的序列化与反序列化。 + * + * @param formatter 用于序列化 {@link LocalDate} 的格式化器,不可为 {@code null}。 + */ + public LocalDateTypeAdapter(DateTimeFormatter formatter) { + AssertTools.checkArgumentNotNull(formatter, "formatter can not be null."); + this.dateTimeFormatter = formatter; + } + + /** {@inheritDoc} */ + @Override + public void write(JsonWriter out, LocalDate value) throws IOException { + out.value(dateTimeFormatter.format(value)); + } + + /** {@inheritDoc} */ + @Override + public LocalDate read(JsonReader in) throws IOException { + return LocalDate.parse(in.nextString(), dateTimeFormatter); + } +} diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/ZonedDateTimeTypeAdapter.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/ZonedDateTimeTypeAdapter.java new file mode 100644 index 0000000..7acd75e --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/ZonedDateTimeTypeAdapter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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 xyz.zhouxy.plusone.commons.gson; + +import java.io.IOException; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import xyz.zhouxy.plusone.commons.util.AssertTools; + +/** + * {@code ZonedDateTime} 的 {@code TypeAdapter}, + * 用于 Gson 对 {@code ZonedDateTime} 进行相互转换。 + * + * @author ZhouXY + * @since 1.1.0 + * @see TypeAdapter + * @see com.google.gson.GsonBuilder + */ +public class ZonedDateTimeTypeAdapter extends TypeAdapter { + + private final DateTimeFormatter dateTimeFormatter; + + /** + * 默认构造函数, + * 使用 {@link DateTimeFormatter#ISO_ZONED_DATE_TIME} 进行 {@link ZonedDateTime} 的序列化与反序列化。 + */ + public ZonedDateTimeTypeAdapter() { + this.dateTimeFormatter = DateTimeFormatter.ISO_ZONED_DATE_TIME; + } + + /** + * 构造函数, + * 使用传入的 {@link DateTimeFormatter} 进行 {@link ZonedDateTime} 的序列化与反序列化。 + * + * @param formatter 用于序列化 {@link ZonedDateTime} 的格式化器,不可为 {@code null}。 + */ + public ZonedDateTimeTypeAdapter(DateTimeFormatter formatter) { + AssertTools.checkArgumentNotNull(formatter, "formatter can not be null."); + this.dateTimeFormatter = formatter; + } + + /** {@inheritDoc} */ + @Override + public void write(JsonWriter out, ZonedDateTime value) throws IOException { + out.value(dateTimeFormatter.format(value)); + } + + /** {@inheritDoc} */ + @Override + public ZonedDateTime read(JsonReader in) throws IOException { + return ZonedDateTime.parse(in.nextString(), dateTimeFormatter); + } +} diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/GsonTypeAdapterTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/GsonTypeAdapterTests.java new file mode 100644 index 0000000..b1d2022 --- /dev/null +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/GsonTypeAdapterTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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 xyz.zhouxy.plusone.commons.gson; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; + +import org.junit.jupiter.api.Test; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class GsonTypeAdapterTests { + + final Gson gsonWithDefaultFormatter = new GsonBuilder() + .registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter().nullSafe()) + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter().nullSafe()) + .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter().nullSafe()) + .create(); + + final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd"); + final DateTimeFormatter localDateTimeFormatter = new DateTimeFormatterBuilder() + .appendPattern("yyyy/MM/dd HH:mm:ss") + .appendValue(ChronoField.MILLI_OF_SECOND, 3) + .toFormatter(); + final DateTimeFormatter zonedDateTimeFormatter = new DateTimeFormatterBuilder() + .appendPattern("yyyy/MM/dd HH:mm:ss") + .appendValue(ChronoField.MILLI_OF_SECOND, 3) + .appendZoneId() + .toFormatter(); + final Gson gsonWithSpecifiedFormatter = new GsonBuilder() + .registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter(dateFormatter).nullSafe()) + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter(localDateTimeFormatter).nullSafe()) + .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter(zonedDateTimeFormatter).nullSafe()) + .create(); + + final LocalDate date = LocalDate.of(2025, 6, 6); + final LocalDateTime localDateTime = date.atTime(6, 6, 6, 666000000); + final ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of("+08:00")); + + @Test + void test_serialize_defaultFormatter() { + Foo foo = new Foo(); + foo.localDate = date; + foo.localDateTime = localDateTime; + foo.zonedDateTime = zonedDateTime; + + String json = String.format( + "{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\"}", + DateTimeFormatter.ISO_LOCAL_DATE.format(date), + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localDateTime), + DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zonedDateTime) + ); + + assertEquals(json, gsonWithDefaultFormatter.toJson(foo)); + } + + @Test + void test_serialize_specifiedFormatter() { + Foo foo = new Foo(); + foo.localDate = date; + foo.localDateTime = localDateTime; + foo.zonedDateTime = zonedDateTime; + + String json = String.format( + "{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\"}", + dateFormatter.format(date), + localDateTimeFormatter.format(localDateTime), + zonedDateTimeFormatter.format(zonedDateTime) + ); + + assertEquals(json, gsonWithSpecifiedFormatter.toJson(foo)); + } + + @Test + void test_deserialize_defaultFormatter() { + String json = String.format( + "{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\"}", + DateTimeFormatter.ISO_LOCAL_DATE.format(date), + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localDateTime), + DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zonedDateTime) + ); + Foo foo = gsonWithDefaultFormatter.fromJson(json, Foo.class); + assertEquals(date, foo.localDate); + assertEquals(localDateTime, foo.localDateTime); + assertEquals(zonedDateTime, foo.zonedDateTime); + } + + @Test + void test_deserialize_specifiedFormatter() { + String json = String.format( + "{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\"}", + dateFormatter.format(date), + localDateTimeFormatter.format(localDateTime), + zonedDateTimeFormatter.format(zonedDateTime) + ); + Foo foo = gsonWithSpecifiedFormatter.fromJson(json, Foo.class); + assertEquals(date, foo.localDate); + assertEquals(localDateTime, foo.localDateTime); + assertEquals(zonedDateTime, foo.zonedDateTime); + } + + static class Foo { + LocalDate localDate; + LocalDateTime localDateTime; + ZonedDateTime zonedDateTime; + } +} From 2827f69aef916135f70ba9af72179ca785877e62 Mon Sep 17 00:00:00 2001 From: ZhouXY108 Date: Sat, 7 Jun 2025 00:22:04 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat(gson):=20=E6=96=B0=E5=A2=9E=20`Instant?= =?UTF-8?q?TypeAdapter`=20=E7=94=A8=E4=BA=8E=20Gson=20=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96=E5=92=8C=E5=8F=8D=E5=BA=8F=E5=88=97=E5=8C=96=20`Insta?= =?UTF-8?q?nt`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `InstantTypeAdapter` 用于 Gson 通过 `DateTimeFormatter#ISO_INSTANT` 序列化和反序列化 `Instant` 类型。 --- .../commons/gson/InstantTypeAdapter.java | 52 +++++++++++++++++++ .../commons/gson/GsonTypeAdapterTests.java | 21 ++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/InstantTypeAdapter.java diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/InstantTypeAdapter.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/InstantTypeAdapter.java new file mode 100644 index 0000000..24e6fb3 --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/InstantTypeAdapter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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 xyz.zhouxy.plusone.commons.gson; + +import java.io.IOException; +import java.time.Instant; +import java.time.format.DateTimeFormatter; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * {@code Instant} 的 {@code TypeAdapter}, + * 用于 Gson 对 {@code Instant} 进行相互转换。 + * + *

+ * 使用 {@link DateTimeFormatter#ISO_INSTANT} 进行 {@link Instant} 的序列化与反序列化。 + *

+ * + * @author ZhouXY + * @since 1.1.0 + * @see TypeAdapter + * @see com.google.gson.GsonBuilder + */ +public final class InstantTypeAdapter extends TypeAdapter { + + /** {@inheritDoc} */ + @Override + public void write(JsonWriter out, Instant value) throws IOException { + out.value(DateTimeFormatter.ISO_INSTANT.format(value)); + } + + /** {@inheritDoc} */ + @Override + public Instant read(JsonReader in) throws IOException { + return Instant.parse(in.nextString()); + } +} diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/GsonTypeAdapterTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/GsonTypeAdapterTests.java index b1d2022..b00f5da 100644 --- a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/GsonTypeAdapterTests.java +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/GsonTypeAdapterTests.java @@ -17,6 +17,7 @@ package xyz.zhouxy.plusone.commons.gson; import static org.junit.jupiter.api.Assertions.assertEquals; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; @@ -25,6 +26,7 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import com.google.gson.Gson; @@ -39,6 +41,7 @@ public class GsonTypeAdapterTests { .registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter().nullSafe()) .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter().nullSafe()) .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeTypeAdapter().nullSafe()) + .registerTypeAdapter(Instant.class, new InstantTypeAdapter().nullSafe()) .create(); final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd"); @@ -60,24 +63,29 @@ public class GsonTypeAdapterTests { final LocalDate date = LocalDate.of(2025, 6, 6); final LocalDateTime localDateTime = date.atTime(6, 6, 6, 666000000); final ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of("+08:00")); + final Instant instant = zonedDateTime.toInstant(); + @DisplayName("测试使用 TypeAdapter 中默认的 formatter 进行序列化") @Test void test_serialize_defaultFormatter() { Foo foo = new Foo(); foo.localDate = date; foo.localDateTime = localDateTime; foo.zonedDateTime = zonedDateTime; + foo.instant = instant; String json = String.format( - "{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\"}", + "{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\",\"instant\":\"%s\"}", DateTimeFormatter.ISO_LOCAL_DATE.format(date), DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localDateTime), - DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zonedDateTime) + DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zonedDateTime), + DateTimeFormatter.ISO_INSTANT.format(instant) ); assertEquals(json, gsonWithDefaultFormatter.toJson(foo)); } + @DisplayName("测试指定 formatter 进行序列化") @Test void test_serialize_specifiedFormatter() { Foo foo = new Foo(); @@ -95,20 +103,24 @@ public class GsonTypeAdapterTests { assertEquals(json, gsonWithSpecifiedFormatter.toJson(foo)); } + @DisplayName("测试使用 TypeAdapter 中默认的 formatter 进行反序列化") @Test void test_deserialize_defaultFormatter() { String json = String.format( - "{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\"}", + "{\"localDate\":\"%s\",\"localDateTime\":\"%s\",\"zonedDateTime\":\"%s\",\"instant\":\"%s\"}", DateTimeFormatter.ISO_LOCAL_DATE.format(date), DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(localDateTime), - DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zonedDateTime) + DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zonedDateTime), + DateTimeFormatter.ISO_INSTANT.format(instant) ); Foo foo = gsonWithDefaultFormatter.fromJson(json, Foo.class); assertEquals(date, foo.localDate); assertEquals(localDateTime, foo.localDateTime); assertEquals(zonedDateTime, foo.zonedDateTime); + assertEquals(instant, foo.instant); } + @DisplayName("测试指定 formatter 进行反序列化") @Test void test_deserialize_specifiedFormatter() { String json = String.format( @@ -127,5 +139,6 @@ public class GsonTypeAdapterTests { LocalDate localDate; LocalDateTime localDateTime; ZonedDateTime zonedDateTime; + Instant instant; } } From 0731bf2c2274e648409eb43cddeb53fee27a60ca Mon Sep 17 00:00:00 2001 From: ZhouXY108 Date: Sat, 7 Jun 2025 00:56:11 +0800 Subject: [PATCH 3/3] =?UTF-8?q?refactor(gson):=20=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E5=A4=9A=E4=B8=AA=20JSR-310=20=E7=B1=BB=E5=9E=8B=E9=80=82?= =?UTF-8?q?=E9=85=8D=E5=99=A8=E5=88=B0=20`JSR310TypeAdapters`=20=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 InstantTypeAdapter、LocalDateTimeTypeAdapter、LocalDateTypeAdapter 和 ZonedDateTimeTypeAdapter 合并到 JSR310TypeAdapters 类中 - 更新包结构,移动适配器到 adapter 子包 - 新增相关包的 javadoc --- .../commons/gson/InstantTypeAdapter.java | 52 ----- .../gson/LocalDateTimeTypeAdapter.java | 71 ------- .../commons/gson/LocalDateTypeAdapter.java | 71 ------- .../gson/ZonedDateTimeTypeAdapter.java | 71 ------- .../gson/adapter/JSR310TypeAdapters.java | 187 ++++++++++++++++++ .../commons/gson/adapter/package-info.java | 20 ++ .../plusone/commons/gson/package-info.java | 20 ++ .../JSR310TypeAdaptersTests.java} | 5 +- 8 files changed, 230 insertions(+), 267 deletions(-) delete mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/InstantTypeAdapter.java delete mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTimeTypeAdapter.java delete mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTypeAdapter.java delete mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/ZonedDateTimeTypeAdapter.java create mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/adapter/JSR310TypeAdapters.java create mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/adapter/package-info.java create mode 100644 plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/package-info.java rename plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/{GsonTypeAdapterTests.java => adapter/JSR310TypeAdaptersTests.java} (97%) diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/InstantTypeAdapter.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/InstantTypeAdapter.java deleted file mode 100644 index 24e6fb3..0000000 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/InstantTypeAdapter.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2025 the original author or authors. - * - * 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 - * - * https://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 xyz.zhouxy.plusone.commons.gson; - -import java.io.IOException; -import java.time.Instant; -import java.time.format.DateTimeFormatter; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -/** - * {@code Instant} 的 {@code TypeAdapter}, - * 用于 Gson 对 {@code Instant} 进行相互转换。 - * - *

- * 使用 {@link DateTimeFormatter#ISO_INSTANT} 进行 {@link Instant} 的序列化与反序列化。 - *

- * - * @author ZhouXY - * @since 1.1.0 - * @see TypeAdapter - * @see com.google.gson.GsonBuilder - */ -public final class InstantTypeAdapter extends TypeAdapter { - - /** {@inheritDoc} */ - @Override - public void write(JsonWriter out, Instant value) throws IOException { - out.value(DateTimeFormatter.ISO_INSTANT.format(value)); - } - - /** {@inheritDoc} */ - @Override - public Instant read(JsonReader in) throws IOException { - return Instant.parse(in.nextString()); - } -} diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTimeTypeAdapter.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTimeTypeAdapter.java deleted file mode 100644 index 317f404..0000000 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTimeTypeAdapter.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2025 the original author or authors. - * - * 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 - * - * https://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 xyz.zhouxy.plusone.commons.gson; - -import java.io.IOException; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -import xyz.zhouxy.plusone.commons.util.AssertTools; - -/** - * {@code LocalDateTime} 的 {@code TypeAdapter}, - * 用于 Gson 对 {@code LocalDateTime} 进行相互转换。 - * - * @author ZhouXY - * @since 1.1.0 - * @see TypeAdapter - * @see com.google.gson.GsonBuilder - */ -public class LocalDateTimeTypeAdapter extends TypeAdapter { - - private final DateTimeFormatter dateTimeFormatter; - - /** - * 默认构造函数, - * 使用 {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} 进行 {@link LocalDateTime} 的序列化与反序列化。 - */ - public LocalDateTimeTypeAdapter() { - this.dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; - } - - /** - * 构造函数, - * 使用传入的 {@link DateTimeFormatter} 进行 {@link LocalDateTime} 的序列化与反序列化。 - * - * @param formatter 用于序列化 {@link LocalDateTime} 的格式化器,不可为 {@code null}。 - */ - public LocalDateTimeTypeAdapter(DateTimeFormatter formatter) { - AssertTools.checkArgumentNotNull(formatter, "formatter can not be null."); - this.dateTimeFormatter = formatter; - } - - /** {@inheritDoc} */ - @Override - public void write(JsonWriter out, LocalDateTime value) throws IOException { - out.value(dateTimeFormatter.format(value)); - } - - /** {@inheritDoc} */ - @Override - public LocalDateTime read(JsonReader in) throws IOException { - return LocalDateTime.parse(in.nextString(), dateTimeFormatter); - } -} diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTypeAdapter.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTypeAdapter.java deleted file mode 100644 index 5a42ad3..0000000 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/LocalDateTypeAdapter.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2025 the original author or authors. - * - * 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 - * - * https://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 xyz.zhouxy.plusone.commons.gson; - -import java.io.IOException; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -import xyz.zhouxy.plusone.commons.util.AssertTools; - -/** - * {@code LocalDate} 的 {@code TypeAdapter}, - * 用于 Gson 对 {@code LocalDate} 进行相互转换。 - * - * @author ZhouXY - * @since 1.1.0 - * @see TypeAdapter - * @see com.google.gson.GsonBuilder - */ -public class LocalDateTypeAdapter extends TypeAdapter { - - private final DateTimeFormatter dateTimeFormatter; - - /** - * 默认构造函数, - * 使用 {@link DateTimeFormatter#ISO_LOCAL_DATE} 进行 {@link LocalDate} 的序列化与反序列化。 - */ - public LocalDateTypeAdapter() { - this.dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE; - } - - /** - * 构造函数, - * 使用传入的 {@link DateTimeFormatter} 进行 {@link LocalDate} 的序列化与反序列化。 - * - * @param formatter 用于序列化 {@link LocalDate} 的格式化器,不可为 {@code null}。 - */ - public LocalDateTypeAdapter(DateTimeFormatter formatter) { - AssertTools.checkArgumentNotNull(formatter, "formatter can not be null."); - this.dateTimeFormatter = formatter; - } - - /** {@inheritDoc} */ - @Override - public void write(JsonWriter out, LocalDate value) throws IOException { - out.value(dateTimeFormatter.format(value)); - } - - /** {@inheritDoc} */ - @Override - public LocalDate read(JsonReader in) throws IOException { - return LocalDate.parse(in.nextString(), dateTimeFormatter); - } -} diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/ZonedDateTimeTypeAdapter.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/ZonedDateTimeTypeAdapter.java deleted file mode 100644 index 7acd75e..0000000 --- a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/ZonedDateTimeTypeAdapter.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2025 the original author or authors. - * - * 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 - * - * https://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 xyz.zhouxy.plusone.commons.gson; - -import java.io.IOException; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -import xyz.zhouxy.plusone.commons.util.AssertTools; - -/** - * {@code ZonedDateTime} 的 {@code TypeAdapter}, - * 用于 Gson 对 {@code ZonedDateTime} 进行相互转换。 - * - * @author ZhouXY - * @since 1.1.0 - * @see TypeAdapter - * @see com.google.gson.GsonBuilder - */ -public class ZonedDateTimeTypeAdapter extends TypeAdapter { - - private final DateTimeFormatter dateTimeFormatter; - - /** - * 默认构造函数, - * 使用 {@link DateTimeFormatter#ISO_ZONED_DATE_TIME} 进行 {@link ZonedDateTime} 的序列化与反序列化。 - */ - public ZonedDateTimeTypeAdapter() { - this.dateTimeFormatter = DateTimeFormatter.ISO_ZONED_DATE_TIME; - } - - /** - * 构造函数, - * 使用传入的 {@link DateTimeFormatter} 进行 {@link ZonedDateTime} 的序列化与反序列化。 - * - * @param formatter 用于序列化 {@link ZonedDateTime} 的格式化器,不可为 {@code null}。 - */ - public ZonedDateTimeTypeAdapter(DateTimeFormatter formatter) { - AssertTools.checkArgumentNotNull(formatter, "formatter can not be null."); - this.dateTimeFormatter = formatter; - } - - /** {@inheritDoc} */ - @Override - public void write(JsonWriter out, ZonedDateTime value) throws IOException { - out.value(dateTimeFormatter.format(value)); - } - - /** {@inheritDoc} */ - @Override - public ZonedDateTime read(JsonReader in) throws IOException { - return ZonedDateTime.parse(in.nextString(), dateTimeFormatter); - } -} diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/adapter/JSR310TypeAdapters.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/adapter/JSR310TypeAdapters.java new file mode 100644 index 0000000..6130c95 --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/adapter/JSR310TypeAdapters.java @@ -0,0 +1,187 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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 xyz.zhouxy.plusone.commons.gson.adapter; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import xyz.zhouxy.plusone.commons.util.AssertTools; + +/** + * 包含 JSR-310 相关数据类型的 {@code TypeAdapter} + * + * @author ZhouXY + * @since 1.1.0 + * @see TypeAdapter + * @see com.google.gson.GsonBuilder + */ +public class JSR310TypeAdapters { + + /** + * {@code LocalDate} 的 {@code TypeAdapter}, + * 用于 Gson 对 {@code LocalDate} 进行相互转换。 + */ + public static final class LocalDateTypeAdapter extends TypeAdapter { + + private final DateTimeFormatter dateTimeFormatter; + + /** + * 默认构造函数, + * 使用 {@link DateTimeFormatter#ISO_LOCAL_DATE} 进行 {@link LocalDate} 的序列化与反序列化。 + */ + public LocalDateTypeAdapter() { + this.dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE; + } + + /** + * 构造函数, + * 使用传入的 {@link DateTimeFormatter} 进行 {@link LocalDate} 的序列化与反序列化。 + * + * @param formatter 用于序列化 {@link LocalDate} 的格式化器,不可为 {@code null}。 + */ + public LocalDateTypeAdapter(DateTimeFormatter formatter) { + AssertTools.checkArgumentNotNull(formatter, "formatter can not be null."); + this.dateTimeFormatter = formatter; + } + + /** {@inheritDoc} */ + @Override + public void write(JsonWriter out, LocalDate value) throws IOException { + out.value(dateTimeFormatter.format(value)); + } + + /** {@inheritDoc} */ + @Override + public LocalDate read(JsonReader in) throws IOException { + return LocalDate.parse(in.nextString(), dateTimeFormatter); + } + } + + /** + * {@code LocalDateTime} 的 {@code TypeAdapter}, + * 用于 Gson 对 {@code LocalDateTime} 进行相互转换。 + */ + public static final class LocalDateTimeTypeAdapter extends TypeAdapter { + + private final DateTimeFormatter dateTimeFormatter; + + /** + * 默认构造函数, + * 使用 {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} 进行 {@link LocalDateTime} 的序列化与反序列化。 + */ + public LocalDateTimeTypeAdapter() { + this.dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + } + + /** + * 构造函数, + * 使用传入的 {@link DateTimeFormatter} 进行 {@link LocalDateTime} 的序列化与反序列化。 + * + * @param formatter 用于序列化 {@link LocalDateTime} 的格式化器,不可为 {@code null}。 + */ + public LocalDateTimeTypeAdapter(DateTimeFormatter formatter) { + AssertTools.checkArgumentNotNull(formatter, "formatter can not be null."); + this.dateTimeFormatter = formatter; + } + + /** {@inheritDoc} */ + @Override + public void write(JsonWriter out, LocalDateTime value) throws IOException { + out.value(dateTimeFormatter.format(value)); + } + + /** {@inheritDoc} */ + @Override + public LocalDateTime read(JsonReader in) throws IOException { + return LocalDateTime.parse(in.nextString(), dateTimeFormatter); + } + } + + /** + * {@code ZonedDateTime} 的 {@code TypeAdapter}, + * 用于 Gson 对 {@code ZonedDateTime} 进行相互转换。 + */ + public static final class ZonedDateTimeTypeAdapter extends TypeAdapter { + + private final DateTimeFormatter dateTimeFormatter; + + /** + * 默认构造函数, + * 使用 {@link DateTimeFormatter#ISO_ZONED_DATE_TIME} 进行 {@link ZonedDateTime} 的序列化与反序列化。 + */ + public ZonedDateTimeTypeAdapter() { + this.dateTimeFormatter = DateTimeFormatter.ISO_ZONED_DATE_TIME; + } + + /** + * 构造函数, + * 使用传入的 {@link DateTimeFormatter} 进行 {@link ZonedDateTime} 的序列化与反序列化。 + * + * @param formatter 用于序列化 {@link ZonedDateTime} 的格式化器,不可为 {@code null}。 + */ + public ZonedDateTimeTypeAdapter(DateTimeFormatter formatter) { + AssertTools.checkArgumentNotNull(formatter, "formatter can not be null."); + this.dateTimeFormatter = formatter; + } + + /** {@inheritDoc} */ + @Override + public void write(JsonWriter out, ZonedDateTime value) throws IOException { + out.value(dateTimeFormatter.format(value)); + } + + /** {@inheritDoc} */ + @Override + public ZonedDateTime read(JsonReader in) throws IOException { + return ZonedDateTime.parse(in.nextString(), dateTimeFormatter); + } + } + + /** + * {@code Instant} 的 {@code TypeAdapter}, + * 用于 Gson 对 {@code Instant} 进行相互转换。 + * + *

+ * 使用 {@link DateTimeFormatter#ISO_INSTANT} 进行 {@link Instant} 的序列化与反序列化。 + *

+ */ + public static final class InstantTypeAdapter extends TypeAdapter { + + /** {@inheritDoc} */ + @Override + public void write(JsonWriter out, Instant value) throws IOException { + out.value(DateTimeFormatter.ISO_INSTANT.format(value)); + } + + /** {@inheritDoc} */ + @Override + public Instant read(JsonReader in) throws IOException { + return Instant.parse(in.nextString()); + } + } + + private JSR310TypeAdapters() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/adapter/package-info.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/adapter/package-info.java new file mode 100644 index 0000000..00356dd --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/adapter/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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. + */ + +/** + * Gson 相关类型适配器 + */ +package xyz.zhouxy.plusone.commons.gson.adapter; diff --git a/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/package-info.java b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/package-info.java new file mode 100644 index 0000000..221b760 --- /dev/null +++ b/plusone-commons/src/main/java/xyz/zhouxy/plusone/commons/gson/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2025 the original author or authors. + * + * 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 + * + * https://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. + */ + +/** + * Gson 相关辅助工具 + */ +package xyz.zhouxy.plusone.commons.gson; diff --git a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/GsonTypeAdapterTests.java b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/adapter/JSR310TypeAdaptersTests.java similarity index 97% rename from plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/GsonTypeAdapterTests.java rename to plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/adapter/JSR310TypeAdaptersTests.java index b00f5da..c9cb1f8 100644 --- a/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/GsonTypeAdapterTests.java +++ b/plusone-commons/src/test/java/xyz/zhouxy/plusone/commons/gson/adapter/JSR310TypeAdaptersTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package xyz.zhouxy.plusone.commons.gson; +package xyz.zhouxy.plusone.commons.gson.adapter; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -33,9 +33,10 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import lombok.extern.slf4j.Slf4j; +import xyz.zhouxy.plusone.commons.gson.adapter.JSR310TypeAdapters.*; @Slf4j -public class GsonTypeAdapterTests { +public final class JSR310TypeAdaptersTests { final Gson gsonWithDefaultFormatter = new GsonBuilder() .registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter().nullSafe())