Skip to content

Commit

Permalink
Prevent deserialization of "" as null for Duration, Instant, `L…
Browse files Browse the repository at this point in the history
…ocalTime`, `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
  • Loading branch information
kupci authored and cowtowncoder committed Oct 18, 2019
1 parent 1f32a9f commit f063193
Show file tree
Hide file tree
Showing 22 changed files with 775 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,31 @@

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

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.datatype.jsr310.DecimalUtils;

import java.io.IOException;
import java.math.BigDecimal;
import java.time.DateTimeException;
import java.time.Duration;


/**
* Deserializer for Java 8 temporal {@link Duration}s.
*
* @author Nick Williams
* @since 2.2.0
*/
public class DurationDeserializer extends JSR310DeserializerBase<Duration>
public class DurationDeserializer extends JSR310DeserializerBase<Duration> implements ContextualDeserializer
{
private static final long serialVersionUID = 1L;

Expand All @@ -45,6 +51,18 @@ private DurationDeserializer()
super(Duration.class);
}

/**
* Since 2.11
*/
protected DurationDeserializer(DurationDeserializer base, Boolean leniency) {
super(base, leniency);
}

@Override
protected DurationDeserializer withLeniency(Boolean leniency) {
return new DurationDeserializer(this, leniency);
}

@Override
public Duration deserialize(JsonParser parser, DeserializationContext context) throws IOException
{
Expand All @@ -63,6 +81,9 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t
case JsonTokenId.ID_STRING:
String string = parser.getText().trim();
if (string.length() == 0) {
if (!isLenient()) {
return _failForNotLenient(parser, context, JsonToken.VALUE_STRING);
}
return null;
}
try {
Expand All @@ -81,4 +102,21 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t
return _handleUnexpectedToken(context, parser, JsonToken.VALUE_STRING,
JsonToken.VALUE_NUMBER_INT, JsonToken.VALUE_NUMBER_FLOAT);
}

@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
BeanProperty property) throws JsonMappingException
{
JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
DurationDeserializer deser = this;
if (format != null) {
if (format.hasLenient()) {
Boolean leniency = format.getLenient();
if (leniency != null) {
deser = deser.withLeniency(leniency);
}
}
}
return deser;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,18 @@ protected InstantDeserializer(InstantDeserializer<T> base, Boolean adjustToConte
_adjustToContextTZOverride = adjustToContextTimezoneOverride;
}

@SuppressWarnings("unchecked")
protected InstantDeserializer(InstantDeserializer<T> base, DateTimeFormatter f, Boolean leniency)
{
super((Class<T>) base.handledType(), f, leniency);
parsedToValue = base.parsedToValue;
fromMilliseconds = base.fromMilliseconds;
fromNanoseconds = base.fromNanoseconds;
adjust = base.adjust;
replaceZeroOffsetAsZ = (_formatter == DateTimeFormatter.ISO_INSTANT);
_adjustToContextTZOverride = base._adjustToContextTZOverride;
}

@Override
protected InstantDeserializer<T> withDateFormat(DateTimeFormatter dtf) {
if (dtf == _formatter) {
Expand All @@ -159,10 +171,12 @@ protected InstantDeserializer<T> withDateFormat(DateTimeFormatter dtf) {
return new InstantDeserializer<T>(this, dtf);
}

// !!! TODO: lenient vs strict?
@Override
protected InstantDeserializer<T> withLeniency(Boolean leniency) {
return this;
if (_isLenient == !Boolean.FALSE.equals(leniency)) {
return this;
}
return new InstantDeserializer<T>(this, _formatter, leniency);
}

@Override
Expand All @@ -186,6 +200,9 @@ public T deserialize(JsonParser parser, DeserializationContext context) throws I
{
String string = parser.getText().trim();
if (string.length() == 0) {
if (!isLenient()) {
return _failForNotLenient(parser, context, JsonToken.VALUE_STRING);
}
return null;
}
// only check for other parsing modes if we are using default formatter
Expand Down Expand Up @@ -245,10 +262,16 @@ public JsonDeserializer<T> createContextual(DeserializationContext ctxt,
if (deserializer != this) {
JsonFormat.Value val = findFormatOverrides(ctxt, property, handledType());
if (val != null) {
return new InstantDeserializer<>(deserializer, val.getFeature(JsonFormat.Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE));
deserializer = new InstantDeserializer<>(deserializer, val.getFeature(JsonFormat.Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE));
if (val.hasLenient()) {
Boolean leniency = val.getLenient();
if (leniency != null) {
deserializer = deserializer.withLeniency(leniency);
}
}
}
}
return this;
return deserializer;
}

protected boolean shouldAdjustToContextTimezone(DeserializationContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ protected JSR310DateTimeDeserializerBase(Class<T> supportedType, DateTimeFormatt
_shape = null;
}

/**
* @since 2.11
*/
public JSR310DateTimeDeserializerBase(Class<T> supportedType, DateTimeFormatter f, Boolean leniency) {
super(supportedType);
_formatter = f;
_isLenient = !Boolean.FALSE.equals(leniency);
_shape = null;
}

/**
* @since 2.10
*/
Expand Down Expand Up @@ -93,7 +103,6 @@ protected JSR310DateTimeDeserializerBase(JSR310DateTimeDeserializerBase<T> base,
_isLenient = base._isLenient;
}


protected abstract JSR310DateTimeDeserializerBase<T> withDateFormat(DateTimeFormatter dtf);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.util.ClassUtil;

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

/**
* Flag that indicates what leniency setting is enabled for this deserializer (either
* due {@link JsonFormat} annotation on property or class, or due to per-type
* "config override", or from global settings): leniency/strictness has effect
* on accepting some non-default input value representations (such as integer values
* for dates).
*<p>
* Note that global default setting is for leniency to be enabled, for Jackson 2.x,
* and has to be explicitly change to force strict handling: this is to keep backwards
* compatibility with earlier versions.
*
* @since 2.11
*/
protected final boolean _isLenient;

/**
* @since 2.11
*/
protected JSR310DeserializerBase(Class<T> supportedType) {
super(supportedType);
_isLenient = true;
}

protected JSR310DeserializerBase(Class<T> supportedType,
Boolean leniency) {
super(supportedType);
_isLenient = !Boolean.FALSE.equals(leniency);
}

protected JSR310DeserializerBase(JSR310DeserializerBase<T> base) {
super(base);
_isLenient = true;
}

protected JSR310DeserializerBase(JSR310DeserializerBase<T> base, Boolean leniency) {
super(base);
_isLenient = !Boolean.FALSE.equals(leniency);
}

/**
* @since 2.10
* @since 2.11
*/
protected JSR310DeserializerBase(JSR310DeserializerBase<?> base) {
super(base);
protected abstract JSR310DeserializerBase<T> withLeniency(Boolean leniency);

/**
* @return {@code true} if lenient handling is enabled; {code false} if not (strict mode)
*
* @since 2.11
*/
protected boolean isLenient() {
return _isLenient;
}

@Override
Expand Down Expand Up @@ -122,6 +165,15 @@ protected <R> R _handleUnexpectedToken(DeserializationContext context,
handledType().getName());
}

@SuppressWarnings("unchecked")
protected T _failForNotLenient(JsonParser p, DeserializationContext ctxt,
JsonToken expToken) throws IOException
{
return (T) ctxt.handleUnexpectedToken(handledType(), expToken, p,
"Cannot deserialize instance of %s out of %s token: not allowed because 'strict' mode set for property or type (enable 'lenient' handling to allow)",
ClassUtil.nameOf(handledType()), p.currentToken());
}

/**
* Helper method used to peel off spurious wrappings of DateTimeException
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@
import java.time.ZoneId;
import java.time.ZoneOffset;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonParser;

import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;

/**
Expand All @@ -41,6 +45,7 @@
*/
public class JSR310StringParsableDeserializer
extends JSR310DeserializerBase<Object>
implements ContextualDeserializer
{
private static final long serialVersionUID = 1L;

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

/**
* Since 2.11
*/
protected JSR310StringParsableDeserializer(JSR310StringParsableDeserializer base, Boolean leniency) {
super(base, leniency);
_valueType = base._valueType;
}

@SuppressWarnings("unchecked")
protected static <T> JsonDeserializer<T> createDeserializer(Class<T> type, int typeId) {
return (JsonDeserializer<T>) new JSR310StringParsableDeserializer(type, typeId);
}

@Override
protected JSR310StringParsableDeserializer withLeniency(Boolean leniency) {
if (_isLenient == !Boolean.FALSE.equals(leniency)) {
return this;
}
// TODO: or should this be casting as above in createDeserializer? But then in createContext, we need to
// call the withLeniency method in this class. (See if we can follow InstantDeser convention here?)
return new JSR310StringParsableDeserializer(this, leniency);
}

@Override
public Object deserialize(JsonParser parser, DeserializationContext context) throws IOException
{
if (parser.hasToken(JsonToken.VALUE_STRING)) {
String string = parser.getText().trim();
if (string.length() == 0) {
if (!isLenient()) {
return _failForNotLenient(parser, context, JsonToken.VALUE_STRING);
}
return null;
}
try {
Expand Down Expand Up @@ -118,4 +144,21 @@ public Object deserializeWithType(JsonParser parser, DeserializationContext cont
}
return deserializer.deserializeTypedFromAny(parser, context);
}

@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
BeanProperty property) throws JsonMappingException
{
JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
JSR310StringParsableDeserializer deser = this;
if (format != null) {
if (format.hasLenient()) {
Boolean leniency = format.getLenient();
if (leniency != null) {
deser = this.withLeniency(leniency);
}
}
}
return deser;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,21 @@ public LocalTimeDeserializer(DateTimeFormatter formatter) {
super(LocalTime.class, formatter);
}

/**
* Since 2.11
*/
protected LocalTimeDeserializer(LocalTimeDeserializer base, Boolean leniency) {
super(base, leniency);
}

@Override
protected LocalTimeDeserializer withDateFormat(DateTimeFormatter formatter) {
return new LocalTimeDeserializer(formatter);
}

// !!! TODO: lenient vs strict?
@Override
protected LocalTimeDeserializer withLeniency(Boolean leniency) {
return this;
return new LocalTimeDeserializer(this, leniency);
}

@Override
Expand All @@ -68,6 +74,9 @@ public LocalTime deserialize(JsonParser parser, DeserializationContext context)
if (parser.hasToken(JsonToken.VALUE_STRING)) {
String string = parser.getText().trim();
if (string.length() == 0) {
if (!isLenient()) {
return _failForNotLenient(parser, context, JsonToken.VALUE_STRING);
}
return null;
}
DateTimeFormatter format = _formatter;
Expand Down
Loading

0 comments on commit f063193

Please sign in to comment.