diff --git a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/spanner/GcpSpannerAutoConfigurationTests.java b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/spanner/GcpSpannerAutoConfigurationTests.java index 925af9b691..b50e76b171 100644 --- a/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/spanner/GcpSpannerAutoConfigurationTests.java +++ b/spring-cloud-gcp-autoconfigure/src/test/java/com/google/cloud/spring/autoconfigure/spanner/GcpSpannerAutoConfigurationTests.java @@ -17,7 +17,9 @@ package com.google.cloud.spring.autoconfigure.spanner; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.google.api.gax.core.CredentialsProvider; import com.google.api.gax.retrying.RetrySettings; @@ -31,6 +33,8 @@ import com.google.cloud.spring.data.spanner.core.admin.SpannerSchemaUtils; import com.google.cloud.spring.data.spanner.core.mapping.SpannerMappingContext; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurationPackage; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -46,6 +50,14 @@ class GcpSpannerAutoConfigurationTests { /** Mock Gson object for use in configuration. */ public static Gson MOCK_GSON = mock(Gson.class); + @BeforeAll + static void beforeAll() { + GsonBuilder builderMock = mock(GsonBuilder.class); + when(builderMock.registerTypeAdapter(any(), any())).thenReturn(builderMock); + when(builderMock.create()).thenReturn(MOCK_GSON); + when(MOCK_GSON.newBuilder()).thenReturn(builderMock); + } + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration( diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerMappingContext.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerMappingContext.java index 112c5bad6a..0967400350 100644 --- a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerMappingContext.java +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/SpannerMappingContext.java @@ -18,7 +18,10 @@ import com.google.cloud.spring.data.spanner.core.convert.ConverterAwareMappingSpannerEntityProcessor; import com.google.cloud.spring.data.spanner.core.convert.SpannerEntityProcessor; +import com.google.cloud.spring.data.spanner.core.mapping.typeadapter.InstantTypeAdapter; import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import java.time.Instant; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -50,12 +53,11 @@ public class SpannerMappingContext private Gson gson; - public SpannerMappingContext() { - } + public SpannerMappingContext() {} public SpannerMappingContext(Gson gson) { Assert.notNull(gson, "A non-null gson is required."); - this.gson = gson; + this.gson = addTypeAdapter(gson, new InstantTypeAdapter(), Instant.class); } @NonNull @@ -96,7 +98,8 @@ protected SpannerPersistentEntity createPersistentEntity( protected SpannerPersistentEntityImpl constructPersistentEntity( TypeInformation typeInformation) { SpannerEntityProcessor processor; - if (this.applicationContext == null || !this.applicationContext.containsBean("spannerConverter")) { + if (this.applicationContext == null + || !this.applicationContext.containsBean("spannerConverter")) { processor = new ConverterAwareMappingSpannerEntityProcessor(this); } else { processor = this.applicationContext.getBean(SpannerEntityProcessor.class); @@ -124,4 +127,9 @@ public SpannerPersistentEntity getPersistentEntityOrFail(Class entityClass } return entity; } + + private Gson addTypeAdapter(Gson gson, TypeAdapter typeAdapter, Class type) { + + return gson.newBuilder().registerTypeAdapter(type, typeAdapter).create(); + } } diff --git a/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/typeadapter/InstantTypeAdapter.java b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/typeadapter/InstantTypeAdapter.java new file mode 100644 index 0000000000..996d205cdd --- /dev/null +++ b/spring-cloud-gcp-data-spanner/src/main/java/com/google/cloud/spring/data/spanner/core/mapping/typeadapter/InstantTypeAdapter.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024 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 com.google.cloud.spring.data.spanner.core.mapping.typeadapter; + +import com.google.gson.TypeAdapter; +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.Instant; + +public class InstantTypeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter jsonWriter, Instant instant) throws IOException { + if (instant == null) { + jsonWriter.nullValue(); + } else { + jsonWriter.value(instant.toString()); + } + } + + @Override + public Instant read(JsonReader jsonReader) throws IOException { + if (jsonReader.peek() == JsonToken.NULL) { + return null; + } + return Instant.parse(jsonReader.nextString()); + } +} diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityReaderTests.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityReaderTests.java index a0af0aa706..1550b66971 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityReaderTests.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityReaderTests.java @@ -41,6 +41,7 @@ import com.google.cloud.spring.data.spanner.core.mapping.SpannerDataException; import com.google.cloud.spring.data.spanner.core.mapping.SpannerMappingContext; import com.google.gson.Gson; +import java.time.Instant; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -61,8 +62,7 @@ void setup() { this.spannerReadConverter = new SpannerReadConverter(); SpannerMappingContext mappingContext = new SpannerMappingContext(new Gson()); this.spannerEntityReader = - new ConverterAwareMappingSpannerEntityReader( - mappingContext, this.spannerReadConverter); + new ConverterAwareMappingSpannerEntityReader(mappingContext, this.spannerReadConverter); } @Test @@ -132,8 +132,8 @@ void readArraySingularMismatchTest() { .build(); assertThatThrownBy(() -> this.spannerEntityReader.read(OuterTestEntity.class, rowStruct)) - .isInstanceOf(SpannerDataException.class) - .hasMessage("Column is not an ARRAY type: innerTestEntities"); + .isInstanceOf(SpannerDataException.class) + .hasMessage("Column is not an ARRAY type: innerTestEntities"); } @Test @@ -148,19 +148,23 @@ void readSingularArrayMismatchTest() { Type.struct(StructField.of("string_col", Type.string())), List.of(colStruct)) .build(); - ConverterAwareMappingSpannerEntityReader testReader = new ConverterAwareMappingSpannerEntityReader(new SpannerMappingContext(), new SpannerReadConverter( - List.of( - new Converter() { - @Nullable - @Override - public Integer convert(Struct source) { - return source.getString("string_col").length(); - } - }))); + ConverterAwareMappingSpannerEntityReader testReader = + new ConverterAwareMappingSpannerEntityReader( + new SpannerMappingContext(), + new SpannerReadConverter( + List.of( + new Converter() { + @Nullable + @Override + public Integer convert(Struct source) { + return source.getString("string_col").length(); + } + }))); assertThatThrownBy(() -> testReader.read(OuterTestEntityFlatFaulty.class, rowStruct)) - .isInstanceOf(SpannerDataException.class) - .hasMessage("The value in column with name innerLengths could not be converted to the corresponding" - + " property in the entity. The property's type is class java.lang.Integer."); + .isInstanceOf(SpannerDataException.class) + .hasMessage( + "The value in column with name innerLengths could not be converted to the corresponding" + + " property in the entity. The property's type is class java.lang.Integer."); } @Test @@ -218,8 +222,8 @@ void readNotFoundColumnTest() { .build(); assertThatThrownBy(() -> this.spannerEntityReader.read(TestEntity.class, struct)) - .isInstanceOf(SpannerDataException.class) - .hasMessage("Unable to read column from Cloud Spanner results: id4"); + .isInstanceOf(SpannerDataException.class) + .hasMessage("Unable to read column from Cloud Spanner results: id4"); } @Test @@ -255,11 +259,11 @@ void readUnconvertableValueTest() { .to(Value.bytes(ByteArray.copyFrom("string1"))) .build(); - assertThatThrownBy(() -> this.spannerEntityReader.read(TestEntity.class, struct)) .isInstanceOf(ConversionFailedException.class) - .hasMessage("Failed to convert from type [java.lang.String] to type " - + "[java.lang.Double] for value [UNCONVERTABLE VALUE]") + .hasMessage( + "Failed to convert from type [java.lang.String] to type " + + "[java.lang.Double] for value [UNCONVERTABLE VALUE]") .hasStackTraceContaining( "java.lang.NumberFormatException: For input string: \"UNCONVERTABLEVALUE\""); } @@ -270,8 +274,8 @@ void readUnmatachableTypesTest() { Struct.newBuilder().set("fieldWithUnsupportedType").to(Value.string("key1")).build(); assertThatThrownBy(() -> this.spannerEntityReader.read(FaultyTestEntity.class, struct)) - .isInstanceOf(SpannerDataException.class) - .hasMessage("Unable to read column from Cloud Spanner results: id"); + .isInstanceOf(SpannerDataException.class) + .hasMessage("Unable to read column from Cloud Spanner results: id"); } @Test @@ -329,8 +333,7 @@ void testPartialConstructor() { void ensureConstructorArgsAreReadOnce() { Struct row = mock(Struct.class); when(row.getString("id")).thenReturn("1234"); - when(row.getType()) - .thenReturn(Type.struct(List.of(StructField.of("id", Type.string())))); + when(row.getType()).thenReturn(Type.struct(List.of(StructField.of("id", Type.string())))); when(row.getColumnType("id")).thenReturn(Type.string()); TestEntities.SimpleConstructorTester result = @@ -354,9 +357,10 @@ void testPartialConstructorWithNotEnoughArgs() { .to(Value.float64(3.14)) .build(); - assertThatThrownBy(() -> this.spannerEntityReader.read(TestEntities.PartialConstructor.class, struct)) - .isInstanceOf(SpannerDataException.class) - .hasMessage("Column not found: custom_col"); + assertThatThrownBy( + () -> this.spannerEntityReader.read(TestEntities.PartialConstructor.class, struct)) + .isInstanceOf(SpannerDataException.class) + .hasMessage("Column not found: custom_col"); } @Test @@ -367,11 +371,16 @@ void zeroArgsListShouldNotThrowError() { .set("zeroArgsListOfObjects") .to(Value.stringArray(List.of("hello", "world"))) .build(); - // Starting from Spring 3.0, Collection types without generics can be resolved to type with wildcard - // generics (i.e., "?"). For example, "zeroArgsListOfObjects" will be resolved to List, rather + // Starting from Spring 3.0, Collection types without generics can be resolved to type with + // wildcard + // generics (i.e., "?"). For example, "zeroArgsListOfObjects" will be resolved to List, + // rather // than List. assertThatNoException() - .isThrownBy(() -> this.spannerEntityReader.read(TestEntities.TestEntityWithListWithZeroTypeArgs.class, struct)); + .isThrownBy( + () -> + this.spannerEntityReader.read( + TestEntities.TestEntityWithListWithZeroTypeArgs.class, struct)); } @Test @@ -397,6 +406,28 @@ void readJsonFieldTest() { assertThat(result.params.p2).isEqualTo("5"); } + @Test + void readJsonInstantFieldTest() { + Struct row = mock(Struct.class); + when(row.getString("id")).thenReturn("1234"); + when(row.getType()) + .thenReturn( + Type.struct( + Arrays.asList( + Type.StructField.of("id", Type.string()), + Type.StructField.of("params", Type.json())))); + when(row.getColumnType("id")).thenReturn(Type.string()); + + when(row.getJson("params")).thenReturn("{\"instant\":\"1970-01-01T00:00:00Z\"}"); + + TestEntities.TestEntityInstantInJson result = + this.spannerEntityReader.read(TestEntities.TestEntityInstantInJson.class, row); + + assertThat(result.id).isEqualTo("1234"); + + assertThat(result.params.instant).isEqualTo(Instant.ofEpochSecond(0)); + } + @Test void readArrayJsonFieldTest() { Struct row = mock(Struct.class); @@ -410,9 +441,12 @@ void readArrayJsonFieldTest() { when(row.getColumnType("id")).thenReturn(Type.string()); when(row.getColumnType("paramsList")).thenReturn(Type.array(Type.json())); - when(row.getJsonList("paramsList")).thenReturn( - Arrays.asList("{\"p1\":\"address line\",\"p2\":\"5\"}", - "{\"p1\":\"address line 2\",\"p2\":\"6\"}", null)); + when(row.getJsonList("paramsList")) + .thenReturn( + Arrays.asList( + "{\"p1\":\"address line\",\"p2\":\"5\"}", + "{\"p1\":\"address line 2\",\"p2\":\"6\"}", + null)); TestEntities.TestEntityJsonArray result = this.spannerEntityReader.read(TestEntities.TestEntityJsonArray.class, row); diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriterTests.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriterTests.java index 7bf52d7a98..9cb6ca38e7 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriterTests.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/ConverterAwareMappingSpannerEntityWriterTests.java @@ -77,8 +77,7 @@ void setup() { this.writeConverter = new SpannerWriteConverter(); SpannerMappingContext spannerMappingContext = new SpannerMappingContext(new Gson()); this.spannerEntityWriter = - new ConverterAwareMappingSpannerEntityWriter( - spannerMappingContext, this.writeConverter); + new ConverterAwareMappingSpannerEntityWriter(spannerMappingContext, this.writeConverter); } @Test @@ -367,6 +366,24 @@ void writeJsonTest() { verify(valueBinder).to(Value.json("{\"p1\":\"some value\",\"p2\":\"some other value\"}")); } + @Test + void writeJsonWithInstantTest() { + TestEntities.InstantParam parameters = new TestEntities.InstantParam(Instant.ofEpochSecond(0)); + TestEntities.TestEntityInstantInJson testEntity = + new TestEntities.TestEntityInstantInJson("id1", parameters); + + WriteBuilder writeBuilder = mock(WriteBuilder.class); + ValueBinder valueBinder = mock(ValueBinder.class); + + when(writeBuilder.set("id")).thenReturn(valueBinder); + when(writeBuilder.set("params")).thenReturn(valueBinder); + + this.spannerEntityWriter.write(testEntity, writeBuilder::set); + + verify(valueBinder).to(testEntity.id); + verify(valueBinder).to(Value.json("{\"instant\":\"1970-01-01T00:00:00Z\"}")); + } + @Test void writeNullJsonTest() { TestEntities.TestEntityJson testEntity = new TestEntities.TestEntityJson("id1", null); @@ -386,7 +403,8 @@ void writeNullJsonTest() { @Test void writeJsonArrayTest() { TestEntities.Params parameters = new TestEntities.Params("some value", "some other value"); - TestEntities.TestEntityJsonArray testEntity = new TestEntities.TestEntityJsonArray("id1", Arrays.asList(parameters, parameters)); + TestEntities.TestEntityJsonArray testEntity = + new TestEntities.TestEntityJsonArray("id1", Arrays.asList(parameters, parameters)); WriteBuilder writeBuilder = mock(WriteBuilder.class); ValueBinder valueBinder = mock(ValueBinder.class); @@ -407,7 +425,8 @@ void writeJsonArrayTest() { @Test void writeNullEmptyJsonArrayTest() { TestEntities.TestEntityJsonArray testNull = new TestEntities.TestEntityJsonArray("id1", null); - TestEntities.TestEntityJsonArray testEmpty = new TestEntities.TestEntityJsonArray("id2", new ArrayList<>()); + TestEntities.TestEntityJsonArray testEmpty = + new TestEntities.TestEntityJsonArray("id2", new ArrayList<>()); WriteBuilder writeBuilder = mock(WriteBuilder.class); ValueBinder valueBinder = mock(ValueBinder.class); @@ -432,8 +451,8 @@ void writeUnsupportedTypeIterableTest() { WriteBuilder writeBuilder = Mutation.newInsertBuilder("faulty_test_table_2"); assertThatThrownBy(() -> this.spannerEntityWriter.write(ft, writeBuilder::set)) - .isInstanceOf(SpannerDataException.class) - .hasMessage("Unsupported mapping for type: interface java.util.List"); + .isInstanceOf(SpannerDataException.class) + .hasMessage("Unsupported mapping for type: interface java.util.List"); } @Test @@ -444,18 +463,18 @@ void writeIncompatibleTypeTest() { WriteBuilder writeBuilder = Mutation.newInsertBuilder("faulty_test_table"); assertThatThrownBy(() -> this.spannerEntityWriter.write(ft, writeBuilder::set)) - .isInstanceOf(SpannerDataException.class) - .hasMessage("Unsupported mapping for type: " - + "class com.google.cloud.spring.data.spanner.core.convert.TestEntities$TestEntity"); - + .isInstanceOf(SpannerDataException.class) + .hasMessage( + "Unsupported mapping for type: " + + "class com.google.cloud.spring.data.spanner.core.convert.TestEntities$TestEntity"); } @Test void writingNullToKeyShouldThrowException() { assertThatThrownBy(() -> this.spannerEntityWriter.convertToKey(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Key of an entity to be written cannot be null!"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Key of an entity to be written cannot be null!"); } @Test @@ -471,9 +490,10 @@ void testUserSetUnconvertableColumnType() { new UserSetUnconvertableColumnType(); WriteBuilder writeBuilder = Mutation.newInsertBuilder("faulty_test_table"); - assertThatThrownBy(() -> this.spannerEntityWriter.write(userSetUnconvertableColumnType, writeBuilder::set)) - .isInstanceOf(SpannerDataException.class) - .hasMessage("Unsupported mapping for type: boolean"); + assertThatThrownBy( + () -> this.spannerEntityWriter.write(userSetUnconvertableColumnType, writeBuilder::set)) + .isInstanceOf(SpannerDataException.class) + .hasMessage("Unsupported mapping for type: boolean"); } @Test diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/TestEntities.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/TestEntities.java index 5808aec792..77b62a0ed1 100644 --- a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/TestEntities.java +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/convert/TestEntities.java @@ -276,6 +276,20 @@ static class TestEntityJsonArray { } } + /** A test class with Json field. */ + @Table(name = "json_test_table") + static class TestEntityInstantInJson { + @PrimaryKey String id; + + @Column(spannerType = TypeCode.JSON) + InstantParam params; + + TestEntityInstantInJson(String id, InstantParam params) { + this.id = id; + this.params = params; + } + } + static class Params { String p1; @@ -286,4 +300,12 @@ static class Params { this.p2 = p2; } } + + static class InstantParam { + Instant instant; + + InstantParam(Instant instant) { + this.instant = instant; + } + } } diff --git a/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/mapping/typeadapter/InstantTypeAdapterTest.java b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/mapping/typeadapter/InstantTypeAdapterTest.java new file mode 100644 index 0000000000..b110d9aade --- /dev/null +++ b/spring-cloud-gcp-data-spanner/src/test/java/com/google/cloud/spring/data/spanner/core/mapping/typeadapter/InstantTypeAdapterTest.java @@ -0,0 +1,72 @@ +package com.google.cloud.spring.data.spanner.core.mapping.typeadapter; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.time.Instant; +import org.junit.Test; + +public class InstantTypeAdapterTest { + InstantTypeAdapter instantTypeAdapter = new InstantTypeAdapter(); + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + + @Test + public void writeInstantTest_epochSecond0() throws IOException { + Instant instant = Instant.ofEpochSecond(0); + + instantTypeAdapter.write(jsonWriter, instant); + + assertThat(stringWriter.toString()).isEqualTo("\"1970-01-01T00:00:00Z\""); + + } + + @Test + public void writeInstantTest_epochSecond817() throws IOException { + Instant instant = Instant.ofEpochSecond(817); + + instantTypeAdapter.write(jsonWriter, instant); + + assertThat(stringWriter.toString()).isEqualTo("\"1970-01-01T00:13:37Z\""); + } + + @Test + public void writeNullInstantTest() throws IOException { + Instant instant = null; + + instantTypeAdapter.write(jsonWriter, instant); + + assertThat(stringWriter.toString()).isEqualTo("null"); + } + + @Test + public void readInstantTest_epochSecond0() throws IOException { + StringReader stringReader = new StringReader("\"1970-01-01T00:00:00Z\""); + + Instant readInstant = instantTypeAdapter.read(new JsonReader(stringReader)); + + assertThat(readInstant).isEqualTo(Instant.ofEpochSecond(0)); + } + + @Test + public void readInstantTest_epochSecond42() throws IOException { + StringReader stringReader = new StringReader("\"1970-01-01T00:00:42Z\""); + + Instant readInstant = instantTypeAdapter.read(new JsonReader(stringReader)); + + assertThat(readInstant).isEqualTo(Instant.ofEpochSecond(42)); + } + + @Test + public void readNullInstantTest() throws IOException { + StringReader stringReader = new StringReader("null"); + + Instant readInstant = instantTypeAdapter.read(new JsonReader(stringReader)); + + assertThat(readInstant).isNull(); + } +}