Skip to content

Commit 14e1103

Browse files
authored
Merge pull request #938 from alphagov/PP-13220_better_consistency_for_Instant_serialisers_and_deserialisers
PP-13220 Better consistency for Instant serialisers/deserialisers
2 parents e9011ca + e73ec78 commit 14e1103

11 files changed

+332
-22
lines changed

model/src/main/java/uk/gov/service/payments/commons/api/json/ApiResponseDateTimeSerializer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import java.io.IOException;
88
import java.time.ZonedDateTime;
99

10-
import static uk.gov.service.payments.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION;
10+
import static uk.gov.service.payments.commons.model.CommonDateTimeFormatters.ISO_INSTANT_MILLISECOND_PRECISION;
1111

1212
public class ApiResponseDateTimeSerializer extends StdSerializer<ZonedDateTime> {
1313

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package uk.gov.service.payments.commons.api.json;
2+
3+
import com.fasterxml.jackson.core.JsonParser;
4+
import com.fasterxml.jackson.databind.DeserializationContext;
5+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
6+
7+
import java.io.IOException;
8+
import java.time.Instant;
9+
10+
import static uk.gov.service.payments.commons.model.CommonDateTimeFormatters.ISO_INSTANT_MICROSECOND_PRECISION;
11+
12+
public class IsoInstantMicrosecondDeserializer extends StdDeserializer<Instant> {
13+
14+
public IsoInstantMicrosecondDeserializer() {
15+
this(null);
16+
}
17+
18+
private IsoInstantMicrosecondDeserializer(Class<Instant> t) {
19+
super(t);
20+
}
21+
22+
@Override
23+
public Instant deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
24+
return ISO_INSTANT_MICROSECOND_PRECISION.parse(jsonParser.getText(), Instant::from);
25+
}
26+
27+
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
import java.io.IOException;
88
import java.time.Instant;
99

10-
import static uk.gov.service.payments.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MICROSECOND_PRECISION;
10+
import static uk.gov.service.payments.commons.model.CommonDateTimeFormatters.ISO_INSTANT_MICROSECOND_PRECISION;
1111

12-
public class ApiResponseInstantWithMicrosecondPrecisionSerializer extends StdSerializer<Instant> {
12+
public class IsoInstantMicrosecondSerializer extends StdSerializer<Instant> {
1313

14-
public ApiResponseInstantWithMicrosecondPrecisionSerializer() {
14+
public IsoInstantMicrosecondSerializer() {
1515
this(null);
1616
}
1717

18-
private ApiResponseInstantWithMicrosecondPrecisionSerializer(Class<Instant> t) {
18+
private IsoInstantMicrosecondSerializer(Class<Instant> t) {
1919
super(t);
2020
}
2121

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package uk.gov.service.payments.commons.api.json;
2+
3+
import com.fasterxml.jackson.core.JsonParser;
4+
import com.fasterxml.jackson.databind.DeserializationContext;
5+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
6+
7+
import java.io.IOException;
8+
import java.time.Instant;
9+
10+
import static uk.gov.service.payments.commons.model.CommonDateTimeFormatters.ISO_INSTANT_MILLISECOND_PRECISION;
11+
12+
public class IsoInstantMillisecondDeserializer extends StdDeserializer<Instant> {
13+
14+
public IsoInstantMillisecondDeserializer() {
15+
this(null);
16+
}
17+
18+
private IsoInstantMillisecondDeserializer(Class<Instant> t) {
19+
super(t);
20+
}
21+
22+
@Override
23+
public Instant deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
24+
return ISO_INSTANT_MILLISECOND_PRECISION.parse(jsonParser.getText(), Instant::from);
25+
}
26+
27+
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
import java.io.IOException;
88
import java.time.Instant;
99

10-
import static uk.gov.service.payments.commons.model.ApiResponseDateTimeFormatter.ISO_INSTANT_MILLISECOND_PRECISION;
10+
import static uk.gov.service.payments.commons.model.CommonDateTimeFormatters.ISO_INSTANT_MILLISECOND_PRECISION;
1111

12-
public class ApiResponseInstantSerializer extends StdSerializer<Instant> {
12+
public class IsoInstantMillisecondSerializer extends StdSerializer<Instant> {
1313

14-
public ApiResponseInstantSerializer() {
14+
public IsoInstantMillisecondSerializer() {
1515
this(null);
1616
}
1717

18-
private ApiResponseInstantSerializer(Class<Instant> t) {
18+
private IsoInstantMillisecondSerializer(Class<Instant> t) {
1919
super(t);
2020
}
2121

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import java.time.format.DateTimeFormatterBuilder;
66
import java.util.Locale;
77

8-
public class ApiResponseDateTimeFormatter {
8+
public class CommonDateTimeFormatters {
99

1010
/**
1111
* DateTimeFormatter that produces a standard ISO-8601 format date and time
@@ -27,6 +27,7 @@ public class ApiResponseDateTimeFormatter {
2727
* DateTimeFormatter that produces a standard ISO-8601 local date in UTC
2828
* without an offset, for example 2022-10-04
2929
*/
30-
public static final DateTimeFormatter ISO_LOCAL_DATE_IN_UTC = DateTimeFormatter.ISO_LOCAL_DATE.withZone(ZoneOffset.UTC);
30+
public static final DateTimeFormatter ISO_LOCAL_DATE_IN_UTC =
31+
DateTimeFormatter.ISO_LOCAL_DATE.withZone(ZoneOffset.UTC);
3132

3233
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package uk.gov.service.payments.commons.api.json;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.JsonMappingException;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
7+
import org.junit.jupiter.api.Test;
8+
9+
import java.time.Instant;
10+
11+
import static org.hamcrest.MatcherAssert.assertThat;
12+
import static org.hamcrest.Matchers.nullValue;
13+
import static org.hamcrest.core.Is.is;
14+
import static org.junit.jupiter.api.Assertions.assertThrows;
15+
16+
class IsoInstantMicrosecondDeserializerTest {
17+
18+
private record JsonObjectWithInstant(
19+
@JsonDeserialize(using = IsoInstantMicrosecondDeserializer.class) Instant instant) { }
20+
21+
private final ObjectMapper objectMapper = new ObjectMapper();
22+
23+
@Test
24+
void shouldDeserializeIsoInstantStringWithSecondsWith6DecimalPlacesToInstant() throws JsonProcessingException {
25+
var jsonString = """
26+
{
27+
"instant": "2024-10-02T09:30:00.123456Z"
28+
}
29+
""";
30+
31+
JsonObjectWithInstant deserializedJsonObject = objectMapper.readValue(jsonString, JsonObjectWithInstant.class);
32+
33+
assertThat(deserializedJsonObject.instant(), is(Instant.parse("2024-10-02T09:30:00.123456Z")));
34+
}
35+
36+
@Test
37+
void shouldDeserializeIsoInstantStringWithSecondsWith6DecimalPlacesOfZeroToInstant() throws JsonProcessingException {
38+
var jsonString = """
39+
{
40+
"instant": "2024-10-02T09:30:00.000000Z"
41+
}
42+
""";
43+
44+
JsonObjectWithInstant deserializedJsonObject = objectMapper.readValue(jsonString, JsonObjectWithInstant.class);
45+
46+
assertThat(deserializedJsonObject.instant(), is(Instant.parse("2024-10-02T09:30:00.000Z")));
47+
}
48+
49+
@Test
50+
void shouldDeserializeNullToNull() throws JsonProcessingException {
51+
var jsonString = """
52+
{
53+
"instant": null
54+
}
55+
""";
56+
57+
JsonObjectWithInstant deserializedJsonObject = objectMapper.readValue(jsonString, JsonObjectWithInstant.class);
58+
59+
assertThat(deserializedJsonObject.instant(), is(nullValue()));
60+
}
61+
62+
@Test
63+
void shouldThrowExceptionWhenTryingToDeserializeIsoStringWith5DecimalPlaces() {
64+
var jsonString = """
65+
{
66+
"instant": "2024-10-02T09:30:00.12345Z"
67+
}
68+
""";
69+
70+
assertThrows(JsonMappingException.class, () -> objectMapper.readValue(jsonString, JsonObjectWithInstant.class));
71+
}
72+
73+
@Test
74+
void shouldThrowExceptionWhenTryingToDeserializeIsoStringWith7DecimalPlaces() {
75+
var jsonString = """
76+
{
77+
"instant": "2024-10-02T09:30:00.1234567Z"
78+
}
79+
""";
80+
81+
assertThrows(JsonMappingException.class, () -> objectMapper.readValue(jsonString, JsonObjectWithInstant.class));
82+
}
83+
84+
@Test
85+
void shouldThrowExceptionWhenTryingToDeserializeIsoStringWith3DecimalPlaces() {
86+
var jsonString = """
87+
{
88+
"instant": "2024-10-02T09:30:00.123Z"
89+
}
90+
""";
91+
92+
assertThrows(JsonMappingException.class, () -> objectMapper.readValue(jsonString, JsonObjectWithInstant.class));
93+
}
94+
95+
@Test
96+
void shouldThrowExceptionWhenTryingToDeserializeIsoStringWith6DecimalPlacesButNoTrailingZ() {
97+
var jsonString = """
98+
{
99+
"instant": "2024-10-02T09:30:00.123456"
100+
}
101+
""";
102+
103+
assertThrows(JsonMappingException.class, () -> objectMapper.readValue(jsonString, JsonObjectWithInstant.class));
104+
}
105+
106+
@Test
107+
void shouldThrowExceptionWhenTryingToDeserializeNonsenseString() {
108+
var jsonString = """
109+
{
110+
"instant": "This is clearly not an ISO instant"
111+
}
112+
""";
113+
114+
assertThrows(JsonMappingException.class, () -> objectMapper.readValue(jsonString, JsonObjectWithInstant.class));
115+
}
116+
117+
@Test
118+
void shouldThrowExceptionWhenTryingToDeserializeNonString() {
119+
var jsonString = """
120+
{
121+
"instant": 1727861400123456
122+
}
123+
""";
124+
125+
assertThrows(JsonMappingException.class, () -> objectMapper.readValue(jsonString, JsonObjectWithInstant.class));
126+
}
127+
128+
}
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@
1111
import static org.hamcrest.MatcherAssert.assertThat;
1212
import static org.hamcrest.core.Is.is;
1313

14-
class ApiResponseInstantWithMicrosecondPrecisionSerializerTest {
14+
class IsoInstantMicrosecondSerializerTest {
1515

16-
private final ApiResponseInstantWithMicrosecondPrecisionSerializer apiResponseInstantWithMicrosecondPrecisionSerializer =
17-
new ApiResponseInstantWithMicrosecondPrecisionSerializer();
16+
private final IsoInstantMicrosecondSerializer isoInstantMicrosecondSerializer = new IsoInstantMicrosecondSerializer();
1817

1918
@Test
2019
void shouldSerializeWithMicrosecondPrecision() throws IOException {
@@ -23,7 +22,7 @@ void shouldSerializeWithMicrosecondPrecision() throws IOException {
2322
var stringWriter = new StringWriter();
2423
var jsonGenerator = new JsonFactory().createGenerator(stringWriter);
2524

26-
apiResponseInstantWithMicrosecondPrecisionSerializer.serialize(instant, jsonGenerator, new ObjectMapper().getSerializerProvider());
25+
isoInstantMicrosecondSerializer.serialize(instant, jsonGenerator, new ObjectMapper().getSerializerProvider());
2726
jsonGenerator.flush();
2827

2928
var expected = "\"2020-12-25T15:00:00.123456Z\"";

0 commit comments

Comments
 (0)