Skip to content

Commit f063193

Browse files
kupcicowtowncoder
authored andcommitted
Prevent deserialization of "" as null for Duration, Instant, LocalTime, OffsetTime, Period, ZoneDateTime, ZoneId, ZoneOffset and YearMonth in "strict" (non-lenient) (#144)
Fix #138: Prevent deserialization of "" as `null` for `Duration`, `Instant`, `LocalTime`, `OffsetTime`, and `YearMonth` in "strict" (non-lenient) mode
1 parent 1f32a9f commit f063193

22 files changed

+775
-41
lines changed

Diff for: datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java

+39-1
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,31 @@
1616

1717
package com.fasterxml.jackson.datatype.jsr310.deser;
1818

19+
import com.fasterxml.jackson.annotation.JsonFormat;
1920
import com.fasterxml.jackson.core.JsonParser;
2021
import com.fasterxml.jackson.core.JsonToken;
2122
import com.fasterxml.jackson.core.JsonTokenId;
23+
import com.fasterxml.jackson.databind.BeanProperty;
2224
import com.fasterxml.jackson.databind.DeserializationContext;
2325
import com.fasterxml.jackson.databind.DeserializationFeature;
26+
import com.fasterxml.jackson.databind.JsonDeserializer;
27+
import com.fasterxml.jackson.databind.JsonMappingException;
28+
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
2429
import com.fasterxml.jackson.datatype.jsr310.DecimalUtils;
2530

2631
import java.io.IOException;
2732
import java.math.BigDecimal;
2833
import java.time.DateTimeException;
2934
import java.time.Duration;
3035

36+
3137
/**
3238
* Deserializer for Java 8 temporal {@link Duration}s.
3339
*
3440
* @author Nick Williams
3541
* @since 2.2.0
3642
*/
37-
public class DurationDeserializer extends JSR310DeserializerBase<Duration>
43+
public class DurationDeserializer extends JSR310DeserializerBase<Duration> implements ContextualDeserializer
3844
{
3945
private static final long serialVersionUID = 1L;
4046

@@ -45,6 +51,18 @@ private DurationDeserializer()
4551
super(Duration.class);
4652
}
4753

54+
/**
55+
* Since 2.11
56+
*/
57+
protected DurationDeserializer(DurationDeserializer base, Boolean leniency) {
58+
super(base, leniency);
59+
}
60+
61+
@Override
62+
protected DurationDeserializer withLeniency(Boolean leniency) {
63+
return new DurationDeserializer(this, leniency);
64+
}
65+
4866
@Override
4967
public Duration deserialize(JsonParser parser, DeserializationContext context) throws IOException
5068
{
@@ -63,6 +81,9 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t
6381
case JsonTokenId.ID_STRING:
6482
String string = parser.getText().trim();
6583
if (string.length() == 0) {
84+
if (!isLenient()) {
85+
return _failForNotLenient(parser, context, JsonToken.VALUE_STRING);
86+
}
6687
return null;
6788
}
6889
try {
@@ -81,4 +102,21 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t
81102
return _handleUnexpectedToken(context, parser, JsonToken.VALUE_STRING,
82103
JsonToken.VALUE_NUMBER_INT, JsonToken.VALUE_NUMBER_FLOAT);
83104
}
105+
106+
@Override
107+
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
108+
BeanProperty property) throws JsonMappingException
109+
{
110+
JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
111+
DurationDeserializer deser = this;
112+
if (format != null) {
113+
if (format.hasLenient()) {
114+
Boolean leniency = format.getLenient();
115+
if (leniency != null) {
116+
deser = deser.withLeniency(leniency);
117+
}
118+
}
119+
}
120+
return deser;
121+
}
84122
}

Diff for: datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java

+27-4
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,18 @@ protected InstantDeserializer(InstantDeserializer<T> base, Boolean adjustToConte
151151
_adjustToContextTZOverride = adjustToContextTimezoneOverride;
152152
}
153153

154+
@SuppressWarnings("unchecked")
155+
protected InstantDeserializer(InstantDeserializer<T> base, DateTimeFormatter f, Boolean leniency)
156+
{
157+
super((Class<T>) base.handledType(), f, leniency);
158+
parsedToValue = base.parsedToValue;
159+
fromMilliseconds = base.fromMilliseconds;
160+
fromNanoseconds = base.fromNanoseconds;
161+
adjust = base.adjust;
162+
replaceZeroOffsetAsZ = (_formatter == DateTimeFormatter.ISO_INSTANT);
163+
_adjustToContextTZOverride = base._adjustToContextTZOverride;
164+
}
165+
154166
@Override
155167
protected InstantDeserializer<T> withDateFormat(DateTimeFormatter dtf) {
156168
if (dtf == _formatter) {
@@ -159,10 +171,12 @@ protected InstantDeserializer<T> withDateFormat(DateTimeFormatter dtf) {
159171
return new InstantDeserializer<T>(this, dtf);
160172
}
161173

162-
// !!! TODO: lenient vs strict?
163174
@Override
164175
protected InstantDeserializer<T> withLeniency(Boolean leniency) {
165-
return this;
176+
if (_isLenient == !Boolean.FALSE.equals(leniency)) {
177+
return this;
178+
}
179+
return new InstantDeserializer<T>(this, _formatter, leniency);
166180
}
167181

168182
@Override
@@ -186,6 +200,9 @@ public T deserialize(JsonParser parser, DeserializationContext context) throws I
186200
{
187201
String string = parser.getText().trim();
188202
if (string.length() == 0) {
203+
if (!isLenient()) {
204+
return _failForNotLenient(parser, context, JsonToken.VALUE_STRING);
205+
}
189206
return null;
190207
}
191208
// only check for other parsing modes if we are using default formatter
@@ -245,10 +262,16 @@ public JsonDeserializer<T> createContextual(DeserializationContext ctxt,
245262
if (deserializer != this) {
246263
JsonFormat.Value val = findFormatOverrides(ctxt, property, handledType());
247264
if (val != null) {
248-
return new InstantDeserializer<>(deserializer, val.getFeature(JsonFormat.Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE));
265+
deserializer = new InstantDeserializer<>(deserializer, val.getFeature(JsonFormat.Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE));
266+
if (val.hasLenient()) {
267+
Boolean leniency = val.getLenient();
268+
if (leniency != null) {
269+
deserializer = deserializer.withLeniency(leniency);
270+
}
271+
}
249272
}
250273
}
251-
return this;
274+
return deserializer;
252275
}
253276

254277
protected boolean shouldAdjustToContextTimezone(DeserializationContext context) {

Diff for: datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310DateTimeDeserializerBase.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ protected JSR310DateTimeDeserializerBase(Class<T> supportedType, DateTimeFormatt
6060
_shape = null;
6161
}
6262

63+
/**
64+
* @since 2.11
65+
*/
66+
public JSR310DateTimeDeserializerBase(Class<T> supportedType, DateTimeFormatter f, Boolean leniency) {
67+
super(supportedType);
68+
_formatter = f;
69+
_isLenient = !Boolean.FALSE.equals(leniency);
70+
_shape = null;
71+
}
72+
6373
/**
6474
* @since 2.10
6575
*/
@@ -93,7 +103,6 @@ protected JSR310DateTimeDeserializerBase(JSR310DateTimeDeserializerBase<T> base,
93103
_isLenient = base._isLenient;
94104
}
95105

96-
97106
protected abstract JSR310DateTimeDeserializerBase<T> withDateFormat(DateTimeFormatter dtf);
98107

99108
/**

Diff for: datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310DeserializerBase.java

+55-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.fasterxml.jackson.databind.JsonMappingException;
2828
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
2929
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
30+
import com.fasterxml.jackson.databind.util.ClassUtil;
3031

3132
/**
3233
* Base class that indicates that all JSR310 datatypes are deserialized from scalar JSON types.
@@ -38,15 +39,57 @@ abstract class JSR310DeserializerBase<T> extends StdScalarDeserializer<T>
3839
{
3940
private static final long serialVersionUID = 1L;
4041

42+
/**
43+
* Flag that indicates what leniency setting is enabled for this deserializer (either
44+
* due {@link JsonFormat} annotation on property or class, or due to per-type
45+
* "config override", or from global settings): leniency/strictness has effect
46+
* on accepting some non-default input value representations (such as integer values
47+
* for dates).
48+
*<p>
49+
* Note that global default setting is for leniency to be enabled, for Jackson 2.x,
50+
* and has to be explicitly change to force strict handling: this is to keep backwards
51+
* compatibility with earlier versions.
52+
*
53+
* @since 2.11
54+
*/
55+
protected final boolean _isLenient;
56+
57+
/**
58+
* @since 2.11
59+
*/
4160
protected JSR310DeserializerBase(Class<T> supportedType) {
4261
super(supportedType);
62+
_isLenient = true;
63+
}
64+
65+
protected JSR310DeserializerBase(Class<T> supportedType,
66+
Boolean leniency) {
67+
super(supportedType);
68+
_isLenient = !Boolean.FALSE.equals(leniency);
69+
}
70+
71+
protected JSR310DeserializerBase(JSR310DeserializerBase<T> base) {
72+
super(base);
73+
_isLenient = true;
74+
}
75+
76+
protected JSR310DeserializerBase(JSR310DeserializerBase<T> base, Boolean leniency) {
77+
super(base);
78+
_isLenient = !Boolean.FALSE.equals(leniency);
4379
}
4480

4581
/**
46-
* @since 2.10
82+
* @since 2.11
4783
*/
48-
protected JSR310DeserializerBase(JSR310DeserializerBase<?> base) {
49-
super(base);
84+
protected abstract JSR310DeserializerBase<T> withLeniency(Boolean leniency);
85+
86+
/**
87+
* @return {@code true} if lenient handling is enabled; {code false} if not (strict mode)
88+
*
89+
* @since 2.11
90+
*/
91+
protected boolean isLenient() {
92+
return _isLenient;
5093
}
5194

5295
@Override
@@ -122,6 +165,15 @@ protected <R> R _handleUnexpectedToken(DeserializationContext context,
122165
handledType().getName());
123166
}
124167

168+
@SuppressWarnings("unchecked")
169+
protected T _failForNotLenient(JsonParser p, DeserializationContext ctxt,
170+
JsonToken expToken) throws IOException
171+
{
172+
return (T) ctxt.handleUnexpectedToken(handledType(), expToken, p,
173+
"Cannot deserialize instance of %s out of %s token: not allowed because 'strict' mode set for property or type (enable 'lenient' handling to allow)",
174+
ClassUtil.nameOf(handledType()), p.currentToken());
175+
}
176+
125177
/**
126178
* Helper method used to peel off spurious wrappings of DateTimeException
127179
*

Diff for: datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializer.java

+43
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@
2222
import java.time.ZoneId;
2323
import java.time.ZoneOffset;
2424

25+
import com.fasterxml.jackson.annotation.JsonFormat;
2526
import com.fasterxml.jackson.core.JsonParser;
2627

2728
import com.fasterxml.jackson.core.JsonToken;
29+
import com.fasterxml.jackson.databind.BeanProperty;
2830
import com.fasterxml.jackson.databind.DeserializationContext;
2931
import com.fasterxml.jackson.databind.JsonDeserializer;
32+
import com.fasterxml.jackson.databind.JsonMappingException;
33+
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
3034
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
3135

3236
/**
@@ -41,6 +45,7 @@
4145
*/
4246
public class JSR310StringParsableDeserializer
4347
extends JSR310DeserializerBase<Object>
48+
implements ContextualDeserializer
4449
{
4550
private static final long serialVersionUID = 1L;
4651

@@ -66,17 +71,38 @@ protected JSR310StringParsableDeserializer(Class<?> supportedType, int valueId)
6671
_valueType = valueId;
6772
}
6873

74+
/**
75+
* Since 2.11
76+
*/
77+
protected JSR310StringParsableDeserializer(JSR310StringParsableDeserializer base, Boolean leniency) {
78+
super(base, leniency);
79+
_valueType = base._valueType;
80+
}
81+
6982
@SuppressWarnings("unchecked")
7083
protected static <T> JsonDeserializer<T> createDeserializer(Class<T> type, int typeId) {
7184
return (JsonDeserializer<T>) new JSR310StringParsableDeserializer(type, typeId);
7285
}
7386

87+
@Override
88+
protected JSR310StringParsableDeserializer withLeniency(Boolean leniency) {
89+
if (_isLenient == !Boolean.FALSE.equals(leniency)) {
90+
return this;
91+
}
92+
// TODO: or should this be casting as above in createDeserializer? But then in createContext, we need to
93+
// call the withLeniency method in this class. (See if we can follow InstantDeser convention here?)
94+
return new JSR310StringParsableDeserializer(this, leniency);
95+
}
96+
7497
@Override
7598
public Object deserialize(JsonParser parser, DeserializationContext context) throws IOException
7699
{
77100
if (parser.hasToken(JsonToken.VALUE_STRING)) {
78101
String string = parser.getText().trim();
79102
if (string.length() == 0) {
103+
if (!isLenient()) {
104+
return _failForNotLenient(parser, context, JsonToken.VALUE_STRING);
105+
}
80106
return null;
81107
}
82108
try {
@@ -118,4 +144,21 @@ public Object deserializeWithType(JsonParser parser, DeserializationContext cont
118144
}
119145
return deserializer.deserializeTypedFromAny(parser, context);
120146
}
147+
148+
@Override
149+
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
150+
BeanProperty property) throws JsonMappingException
151+
{
152+
JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
153+
JSR310StringParsableDeserializer deser = this;
154+
if (format != null) {
155+
if (format.hasLenient()) {
156+
Boolean leniency = format.getLenient();
157+
if (leniency != null) {
158+
deser = this.withLeniency(leniency);
159+
}
160+
}
161+
}
162+
return deser;
163+
}
121164
}

Diff for: datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalTimeDeserializer.java

+11-2
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,21 @@ public LocalTimeDeserializer(DateTimeFormatter formatter) {
4848
super(LocalTime.class, formatter);
4949
}
5050

51+
/**
52+
* Since 2.11
53+
*/
54+
protected LocalTimeDeserializer(LocalTimeDeserializer base, Boolean leniency) {
55+
super(base, leniency);
56+
}
57+
5158
@Override
5259
protected LocalTimeDeserializer withDateFormat(DateTimeFormatter formatter) {
5360
return new LocalTimeDeserializer(formatter);
5461
}
5562

56-
// !!! TODO: lenient vs strict?
5763
@Override
5864
protected LocalTimeDeserializer withLeniency(Boolean leniency) {
59-
return this;
65+
return new LocalTimeDeserializer(this, leniency);
6066
}
6167

6268
@Override
@@ -68,6 +74,9 @@ public LocalTime deserialize(JsonParser parser, DeserializationContext context)
6874
if (parser.hasToken(JsonToken.VALUE_STRING)) {
6975
String string = parser.getText().trim();
7076
if (string.length() == 0) {
77+
if (!isLenient()) {
78+
return _failForNotLenient(parser, context, JsonToken.VALUE_STRING);
79+
}
7180
return null;
7281
}
7382
DateTimeFormatter format = _formatter;

0 commit comments

Comments
 (0)