Skip to content

Commit 2b2d794

Browse files
committed
Rework fix for #94, to allow lenience, but test for correct handling
1 parent 1b82082 commit 2b2d794

File tree

6 files changed

+75
-25
lines changed

6 files changed

+75
-25
lines changed

datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateTimeDeserializer.java

+13-8
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@
2222
import java.time.format.DateTimeFormatter;
2323

2424
import com.fasterxml.jackson.annotation.JsonFormat;
25+
2526
import com.fasterxml.jackson.core.JsonParser;
2627
import com.fasterxml.jackson.core.JsonToken;
2728
import com.fasterxml.jackson.core.JsonTokenId;
29+
2830
import com.fasterxml.jackson.databind.DeserializationContext;
2931
import com.fasterxml.jackson.databind.DeserializationFeature;
3032
import com.fasterxml.jackson.databind.JavaType;
31-
import com.fasterxml.jackson.databind.util.ClassUtil;
3233

3334
/**
3435
* Deserializer for Java 8 temporal {@link LocalDateTime}s.
@@ -152,21 +153,25 @@ protected LocalDateTime _fromString(JsonParser p, DeserializationContext ctxt,
152153
return null;
153154
}
154155
try {
155-
// 21-Oct-2020, tatu: Removed as per [modules-base#94];
156-
// original flawed change from [modules-base#56]
156+
// 21-Oct-2020, tatu: Changed as per [modules-base#94] for 2.12,
157+
// had bad timezone handle change from [modules-base#56]
157158
if (_formatter == DEFAULT_FORMATTER) {
159+
// ... only allow iff lenient mode enabled since
158160
// JavaScript by default includes time and zone in JSON serialized Dates (UTC/ISO instant format).
161+
// And if so, do NOT use zoned date parsing as that can easily produce
162+
// incorrect answer.
159163
if (string.length() > 10 && string.charAt(10) == 'T') {
160164
if (string.endsWith("Z")) {
165+
if (isLenient()) {
166+
return LocalDateTime.parse(string.substring(0, string.length()-1),
167+
_formatter);
168+
}
161169
JavaType t = getValueType(ctxt);
162170
return (LocalDateTime) ctxt.handleWeirdStringValue(t.getRawClass(),
163171
string,
164-
"Invalid value ('%s') for %s: should not contain timezone/offset (define custom `@JsonFormat(pattern=)` if you must accept)",
165-
string, ClassUtil.getTypeDescription(t));
172+
"Should not contain offset when 'strict' mode set for property or type (enable 'lenient' handling to allow)"
173+
);
166174
}
167-
// return LocalDateTime.ofInstant(Instant.parse(string), ZoneOffset.UTC);
168-
// } else {
169-
// return LocalDateTime.parse(string, DEFAULT_FORMATTER);
170175
}
171176
}
172177
return LocalDateTime.parse(string, _formatter);

datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/ModuleTestBase.java

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.fasterxml.jackson.datatype.jsr310;
22

3+
import java.time.ZoneId;
34
import java.util.Arrays;
45
import java.util.Collections;
56
import java.util.Map;
@@ -12,6 +13,11 @@
1213

1314
public class ModuleTestBase
1415
{
16+
protected static final ZoneId UTC = ZoneId.of("UTC");
17+
18+
protected static final ZoneId Z_CHICAGO = ZoneId.of("America/Chicago");
19+
protected static final ZoneId Z_BUDAPEST = ZoneId.of("Europe/Budapest");
20+
1521
public static class NoCheckSubTypeValidator
1622
extends PolymorphicTypeValidator.Base
1723
{

datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateTimeDeserTest.java

+48-16
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ public class LocalDateTimeDeserTest
4949
{
5050
private final static ObjectMapper MAPPER = newMapper();
5151
private final static ObjectReader READER = MAPPER.readerFor(LocalDateTime.class);
52+
53+
private final static ObjectMapper STRICT_MAPPER;
54+
static {
55+
STRICT_MAPPER = newMapper();
56+
STRICT_MAPPER.configOverride(LocalDateTime.class)
57+
.setFormat(JsonFormat.Value.forLeniency(false));
58+
}
59+
5260
private final TypeReference<Map<String, LocalDateTime>> MAP_TYPE_REF = new TypeReference<Map<String, LocalDateTime>>() { };
5361

5462
final static class StrictWrapper {
@@ -155,28 +163,56 @@ public void testDeserializationAsString01() throws Exception
155163
public void testDeserializationAsString02() throws Exception
156164
{
157165
LocalDateTime time = LocalDateTime.of(2013, Month.AUGUST, 21, 9, 22, 57);
158-
LocalDateTime value = MAPPER.readValue('"' + time.toString() + '"', LocalDateTime.class);
166+
LocalDateTime value = MAPPER.readValue(q(time.toString()), LocalDateTime.class);
159167
assertEquals("The value is not correct.", time, value);
160168
}
161169

162170
@Test
163171
public void testDeserializationAsString03() throws Exception
164172
{
165173
LocalDateTime time = LocalDateTime.of(2005, Month.NOVEMBER, 5, 22, 31, 5, 829837);
166-
LocalDateTime value = MAPPER.readValue('"' + time.toString() + '"', LocalDateTime.class);
174+
LocalDateTime value = MAPPER.readValue(q(time.toString()), LocalDateTime.class);
167175
assertEquals("The value is not correct.", time, value);
168176
}
169177

170-
// [modules-java#94]: Should not include timezone
178+
/*
179+
/**********************************************************
180+
/* Tests for deserializing from textual representation,
181+
/* fail cases, leniency checking
182+
/**********************************************************
183+
*/
184+
185+
// [modules-java#94]: "Z" offset MAY be allowed, requires leniency
171186
@Test
172-
public void testBadDeserializationOfTimeWithTimeZone() throws Exception
187+
public void testAllowZuluIfLenient() throws Exception
188+
{
189+
final LocalDateTime EXP = LocalDateTime.of(2020, Month.OCTOBER, 22, 4, 16, 20, 504000000);
190+
final String input = q("2020-10-22T04:16:20.504Z");
191+
final ObjectReader r = MAPPER.readerFor(LocalDateTime.class);
192+
193+
// First, defaults:
194+
assertEquals("The value is not correct.", EXP, r.readValue(input));
195+
196+
// but ensure that global timezone setting doesn't matter
197+
LocalDateTime value = r.with(TimeZone.getTimeZone(Z_CHICAGO))
198+
.readValue(input);
199+
assertEquals("The value is not correct.", EXP, value);
200+
201+
value = r.with(TimeZone.getTimeZone(Z_BUDAPEST))
202+
.readValue(input);
203+
assertEquals("The value is not correct.", EXP, value);
204+
}
205+
206+
// [modules-java#94]: "Z" offset not allowed if strict mode
207+
@Test
208+
public void testFailOnZuluIfStrict() throws Exception
173209
{
174210
try {
175-
MAPPER.readValue(q("2020-10-22T00:16:20.504Z"), LocalDateTime.class);
176-
fail("expected fail");
211+
STRICT_MAPPER.readValue(q("2020-10-22T00:16:20.504Z"), LocalDateTime.class);
212+
fail("Should not pass");
177213
} catch (InvalidFormatException e) {
178-
verifyException(e, "Invalid value");
179-
verifyException(e, "for `java.time.LocalDateTime`");
214+
verifyException(e, "Cannot deserialize value of type ");
215+
verifyException(e, "Should not contain offset when 'strict' mode");
180216
}
181217
}
182218

@@ -192,10 +228,9 @@ public void testBadDeserializationAsString01() throws Throwable
192228
}
193229
}
194230

195-
/*
231+
/*
196232
/**********************************************************
197233
/* Tests for empty string handling
198-
*/
199234
/**********************************************************
200235
*/
201236

@@ -224,20 +259,17 @@ public void testLenientDeserializeFromEmptyString() throws Exception {
224259
public void testStrictDeserializeFromEmptyString() throws Exception {
225260

226261
final String key = "datetime";
227-
final ObjectMapper mapper = mapperBuilder().build();
228-
mapper.configOverride(LocalDateTime.class)
229-
.setFormat(JsonFormat.Value.forLeniency(false));
230-
final ObjectReader objectReader = mapper.readerFor(MAP_TYPE_REF);
262+
final ObjectReader objectReader = STRICT_MAPPER.readerFor(MAP_TYPE_REF);
231263
final String dateValAsNullStr = null;
232264

233265
// even with strict, null value should be deserialized without throwing an exception
234-
String valueFromNullStr = mapper.writeValueAsString(asMap(key, dateValAsNullStr));
266+
String valueFromNullStr = STRICT_MAPPER.writeValueAsString(asMap(key, dateValAsNullStr));
235267
Map<String, LocalDateTime> actualMapFromNullStr = objectReader.readValue(valueFromNullStr);
236268
assertNull(actualMapFromNullStr.get(key));
237269

238270
String dateValAsEmptyStr = "";
239271
// TODO: nothing stops us from writing an empty string, maybe there should be a check there too?
240-
String valueFromEmptyStr = mapper.writeValueAsString(asMap("date", dateValAsEmptyStr));
272+
String valueFromEmptyStr = STRICT_MAPPER.writeValueAsString(asMap("date", dateValAsEmptyStr));
241273
// with strict, deserializing an empty string is not permitted
242274
objectReader.readValue(valueFromEmptyStr);
243275
}

datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetDateTimeDeserTest.java

-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public class OffsetDateTimeDeserTest
2929
{
3030
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
3131
private final TypeReference<Map<String, OffsetDateTime>> MAP_TYPE_REF = new TypeReference<Map<String, OffsetDateTime>>() { };
32-
private static final ZoneId UTC = ZoneId.of("UTC");
3332

3433
private static final ZoneId Z1 = ZoneId.of("America/Chicago");
3534

release-notes/CREDITS-2.x

+5
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ Samantha Williamson (samwill@github)
104104
* Contributed fix to #148: Allow strict `LocalDate` parsing
105105
(2.11.0)
106106

107+
Antti S. Lankila (alankila@github)
108+
* Reported #94: Deserialization of timestamps with UTC timezone to LocalDateTime
109+
doesn't yield correct time
110+
(2.12.0)
111+
107112
Joni Syri (jpsyri@github)
108113
* Reported #165: Problem in serializing negative Duration values
109114
(2.12.0)

release-notes/VERSION-2.x

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Modules:
1010

1111
2.12.0 (not yet released)
1212

13+
#94: Deserialization of timestamps with UTC timezone to LocalDateTime
14+
doesn't yield correct time
15+
(reported by Antti L)
1316
#165: Problem in serializing negative Duration values
1417
(reported by Joni S)
1518
#166: Cannot deserialize `OffsetDateTime.MIN` or `OffsetDateTime.MAX` with

0 commit comments

Comments
 (0)