diff options
Diffstat (limited to 'src/main/java/com/fasterxml/jackson/databind/deser')
49 files changed, 2762 insertions, 1203 deletions
diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/AbstractDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/AbstractDeserializer.java index 57a7eba6c..578afd940 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/AbstractDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/AbstractDeserializer.java @@ -16,6 +16,7 @@ import com.fasterxml.jackson.databind.deser.impl.ReadableObjectId; import com.fasterxml.jackson.databind.introspect.AnnotatedMember; import com.fasterxml.jackson.databind.introspect.ObjectIdInfo; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; /** * Deserializer only used for abstract types used as placeholders during polymorphic @@ -187,6 +188,13 @@ handledType().getName())); @Override public boolean isCachable() { return true; } + @Override // since 2.12 + public LogicalType logicalType() { + // 30-May-2020, tatu: Not sure if our choice here matters, but let's + // guess "POJO" is most likely. If need be, could get more creative + return LogicalType.POJO; + } + @Override // since 2.9 public Boolean supportsUpdate(DeserializationConfig config) { /* 23-Oct-2016, tatu: Not exactly sure what to do with this; polymorphic @@ -241,7 +249,7 @@ handledType().getName())); t = p.nextToken(); } if ((t == JsonToken.FIELD_NAME) && _objectIdReader.maySerializeAsObject() - && _objectIdReader.isValidReferencePropertyName(p.getCurrentName(), p)) { + && _objectIdReader.isValidReferencePropertyName(p.currentName(), p)) { return _deserializeFromObjectId(p, ctxt); } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java index c0c544566..96866488b 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java @@ -237,13 +237,8 @@ public abstract class BasicDeserializerFactory } } } - - // Sanity check: does the chosen ValueInstantiator have incomplete creators? - if (instantiator.getIncompleteParameter() != null) { - final AnnotatedParameter nonAnnotatedParam = instantiator.getIncompleteParameter(); - final AnnotatedWithParams ctor = nonAnnotatedParam.getOwner(); - throw new IllegalArgumentException("Argument #"+nonAnnotatedParam.getIndex() - +" of constructor "+ctor+" has no property name annotation; must have name when multiple-parameter constructor annotated as Creator"); + if (instantiator != null) { + instantiator = instantiator.createContextual(ctxt, beanDesc); } return instantiator; diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java index a82b945f8..ced28f0a9 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java @@ -5,6 +5,7 @@ import java.util.*; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.cfg.CoercionAction; import com.fasterxml.jackson.databind.deser.impl.*; import com.fasterxml.jackson.databind.deser.impl.ReadableObjectId.Referring; import com.fasterxml.jackson.databind.util.NameTransformer; @@ -163,7 +164,7 @@ public class BeanDeserializer } return deserializeFromObject(p, ctxt); } - return _deserializeOther(p, ctxt, p.getCurrentToken()); + return _deserializeOther(p, ctxt, p.currentToken()); } protected final Object _deserializeOther(JsonParser p, DeserializationContext ctxt, @@ -398,7 +399,7 @@ public class BeanDeserializer TokenBuffer unknown = null; final Class<?> activeView = _needViewProcesing ? ctxt.getActiveView() : null; - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); List<BeanReferring> referrings = null; for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) { String propName = p.getCurrentName(); @@ -583,23 +584,29 @@ public class BeanDeserializer } return bean; } - if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { - JsonToken t = p.nextToken(); - if (t == JsonToken.END_ARRAY && ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)) { - return null; - } - final Object value = deserialize(p, ctxt); - if (p.nextToken() != JsonToken.END_ARRAY) { - handleMissingEndArrayForSingle(p, ctxt); - } - return value; - } - if (ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)) { + final CoercionAction act = _findCoercionFromEmptyArray(ctxt); + final boolean unwrap = ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS); + + if (unwrap || (act != CoercionAction.Fail)) { JsonToken t = p.nextToken(); if (t == JsonToken.END_ARRAY) { - return null; + switch (act) { + case AsEmpty: + return getEmptyValue(ctxt); + case AsNull: + case TryConvert: + return getNullValue(ctxt); + default: + } + return ctxt.handleUnexpectedToken(getValueType(ctxt), JsonToken.START_ARRAY, p, null); + } + if (unwrap) { + final Object value = deserialize(p, ctxt); + if (p.nextToken() != JsonToken.END_ARRAY) { + handleMissingEndArrayForSingle(p, ctxt); + } + return value; } - return ctxt.handleUnexpectedToken(getValueType(ctxt), JsonToken.START_ARRAY, p, null); } return ctxt.handleUnexpectedToken(getValueType(ctxt), p); } @@ -721,7 +728,7 @@ public class BeanDeserializer Object bean) throws IOException { - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == JsonToken.START_OBJECT) { t = p.nextToken(); } @@ -788,7 +795,7 @@ public class BeanDeserializer TokenBuffer tokens = new TokenBuffer(p, ctxt); tokens.writeStartObject(); - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) { String propName = p.getCurrentName(); p.nextToken(); // to point to value @@ -914,7 +921,7 @@ public class BeanDeserializer final Class<?> activeView = _needViewProcesing ? ctxt.getActiveView() : null; final ExternalTypeHandler ext = _externalTypeIdHandler.start(); - for (JsonToken t = p.getCurrentToken(); t == JsonToken.FIELD_NAME; t = p.nextToken()) { + for (JsonToken t = p.currentToken(); t == JsonToken.FIELD_NAME; t = p.nextToken()) { String propName = p.getCurrentName(); t = p.nextToken(); SettableBeanProperty prop = _beanProperties.find(propName); @@ -970,7 +977,7 @@ public class BeanDeserializer TokenBuffer tokens = new TokenBuffer(p, ctxt); tokens.writeStartObject(); - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) { String propName = p.getCurrentName(); p.nextToken(); // to point to value diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java index ebeeb48ab..fe1008577 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.exc.IgnoredPropertyException; import com.fasterxml.jackson.databind.introspect.*; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.type.ClassKey; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.*; /** @@ -232,8 +233,8 @@ public abstract class BeanDeserializerBase ; // Any transformation we may need to apply? - JsonFormat.Value format = beanDesc.findExpectedFormat(null); - _serializationShape = (format == null) ? null : format.getShape(); + final JsonFormat.Value format = beanDesc.findExpectedFormat(null); + _serializationShape = format.getShape(); _needViewProcesing = hasViews; _vanillaProcessing = !_nonStandardCreation @@ -989,6 +990,19 @@ public abstract class BeanDeserializerBase @Override public boolean isCachable() { return true; } + /** + * Accessor for checking whether this deserializer is operating + * in case-insensitive manner. + * + * @return True if this deserializer should match property names without + * considering casing; false if case has to match exactly. + * + * @since 2.12 + */ + public boolean isCaseInsensitive() { + return _beanProperties.isCaseInsensitive(); + } + @Override // since 2.9 public Boolean supportsUpdate(DeserializationConfig config) { // although with possible caveats, yes, values can be updated @@ -1045,6 +1059,11 @@ public abstract class BeanDeserializerBase @Override public JavaType getValueType() { return _beanType; } + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.POJO; + } + /** * Accessor for iterating over properties this deserializer uses; with * the exception that properties passed via Creator methods @@ -1189,7 +1208,7 @@ public abstract class BeanDeserializerBase } } // or, Object Ids Jackson explicitly sets - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t != null) { // Most commonly, a scalar (int id, uuid String, ...) if (t.isScalarValue()) { @@ -1405,7 +1424,7 @@ public abstract class BeanDeserializerBase return bean; } } - return _valueInstantiator.createFromString(ctxt, p.getText()); + return _deserializeFromString(p, ctxt); } /** @@ -1457,7 +1476,7 @@ public abstract class BeanDeserializerBase return bean; } } - boolean value = (p.getCurrentToken() == JsonToken.VALUE_TRUE); + boolean value = (p.currentToken() == JsonToken.VALUE_TRUE); return _valueInstantiator.createFromBoolean(ctxt, value); } @@ -1510,7 +1529,7 @@ public abstract class BeanDeserializerBase /** * @since 2.9 */ - private final JsonDeserializer<Object> _delegateDeserializer() { + protected final JsonDeserializer<Object> _delegateDeserializer() { JsonDeserializer<Object> deser = _delegateDeserializer; if (deser == null) { deser = _arrayDelegateDeserializer; diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBuilder.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBuilder.java index b43bec3dd..702edd847 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBuilder.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBuilder.java @@ -2,6 +2,7 @@ package com.fasterxml.jackson.databind.deser; import java.util.*; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.fasterxml.jackson.databind.deser.impl.BeanPropertyMap; @@ -10,6 +11,7 @@ import com.fasterxml.jackson.databind.deser.impl.ObjectIdReader; import com.fasterxml.jackson.databind.deser.impl.ValueInjector; import com.fasterxml.jackson.databind.introspect.*; import com.fasterxml.jackson.databind.util.Annotations; +import com.fasterxml.jackson.databind.util.ClassUtil; /** * Builder class used for aggregating deserialization information about @@ -349,7 +351,8 @@ public class BeanDeserializerBuilder Collection<SettableBeanProperty> props = _properties.values(); _fixAccess(props); BeanPropertyMap propertyMap = BeanPropertyMap.construct(_config, props, - _collectAliases(props)); + _collectAliases(props), + _findCaseInsensitivity()); propertyMap.assignIndexes(); // view processing must be enabled if: @@ -404,7 +407,7 @@ public class BeanDeserializerBuilder if (!expBuildMethodName.isEmpty()) { _context.reportBadDefinition(_beanDesc.getType(), String.format("Builder class %s does not have build method (name: '%s')", - _beanDesc.getBeanClass().getName(), + ClassUtil.getTypeDescription(_beanDesc.getType()), expBuildMethodName)); } } else { @@ -415,17 +418,18 @@ public class BeanDeserializerBuilder && !rawBuildType.isAssignableFrom(rawValueType) && !rawValueType.isAssignableFrom(rawBuildType)) { _context.reportBadDefinition(_beanDesc.getType(), - String.format("Build method '%s' has wrong return type (%s), not compatible with POJO type (%s)", + String.format("Build method `%s` has wrong return type (%s), not compatible with POJO type (%s)", _buildMethod.getFullName(), - rawBuildType.getName(), - valueType.getRawClass().getName())); + ClassUtil.getClassDescription(rawBuildType), + ClassUtil.getTypeDescription(valueType))); } } // And if so, we can try building the deserializer Collection<SettableBeanProperty> props = _properties.values(); _fixAccess(props); BeanPropertyMap propertyMap = BeanPropertyMap.construct(_config, props, - _collectAliases(props)); + _collectAliases(props), + _findCaseInsensitivity()); propertyMap.assignIndexes(); boolean anyViews = !_config.isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION); @@ -541,4 +545,15 @@ public class BeanDeserializerBuilder } return mapping; } + + // @since 2.12 + protected boolean _findCaseInsensitivity() { + // 07-May-2020, tatu: First find combination of per-type config overrides (higher + // precedence) and per-type annotations (lower): + JsonFormat.Value format = _beanDesc.findExpectedFormat(null); + // and see if any of those has explicit definition; if not, use global baseline default + Boolean B = format.getFeature(JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES); + return (B == null) ? _config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + : B.booleanValue(); + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index 9c84bedd8..fcf93b1fa 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import com.fasterxml.jackson.databind.introspect.*; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.jsontype.impl.SubTypeValidator; +import com.fasterxml.jackson.databind.util.BeanUtil; import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.jackson.databind.util.SimpleBeanPropertyDefinition; @@ -93,7 +94,7 @@ public class BeanDeserializerFactory throws JsonMappingException { final DeserializationConfig config = ctxt.getConfig(); - // We may also have custom overrides: + // First: we may also have custom overrides: JsonDeserializer<?> deser = _findCustomBeanDeserializer(type, config, beanDesc); if (deser != null) { // [databind#2392] @@ -104,10 +105,8 @@ public class BeanDeserializerFactory } return (JsonDeserializer<Object>) deser; } - /* One more thing to check: do we have an exception type - * (Throwable or its sub-classes)? If so, need slightly - * different handling. - */ + // One more thing to check: do we have an exception type (Throwable or its + // sub-classes)? If so, need slightly different handling. if (type.isThrowable()) { return buildThrowableDeserializer(ctxt, type, beanDesc); } @@ -120,9 +119,8 @@ public class BeanDeserializerFactory // Let's make it possible to materialize abstract types. JavaType concreteType = materializeAbstractType(ctxt, type, beanDesc); if (concreteType != null) { - /* important: introspect actual implementation (abstract class or - * interface doesn't have constructors, for one) - */ + // important: introspect actual implementation (abstract class or + // interface doesn't have constructors, for one) beanDesc = config.introspect(concreteType); return buildBeanDeserializer(ctxt, concreteType, beanDesc); } @@ -139,17 +137,31 @@ public class BeanDeserializerFactory } // For checks like [databind#1599] _validateSubType(ctxt, type, beanDesc); + + // 05-May-2020, tatu: [databind#2683] Let's actually pre-emptively catch + // certain types (for now, java.time.*) to give better error messages + deser = _findUnsupportedTypeDeserializer(ctxt, type, beanDesc); + if (deser != null) { + return (JsonDeserializer<Object>)deser; + } + // Use generic bean introspection to build deserializer return buildBeanDeserializer(ctxt, type, beanDesc); } @Override - public JsonDeserializer<Object> createBuilderBasedDeserializer(DeserializationContext ctxt, - JavaType valueType, BeanDescription beanDesc, Class<?> builderClass) - throws JsonMappingException + public JsonDeserializer<Object> createBuilderBasedDeserializer( + DeserializationContext ctxt, JavaType valueType, BeanDescription beanDesc, + Class<?> builderClass) + throws JsonMappingException { // First: need a BeanDescription for builder class - JavaType builderType = ctxt.constructType(builderClass); + JavaType builderType; + if (ctxt.isEnabled(MapperFeature.INFER_BUILDER_TYPE_BINDINGS)) { + builderType = ctxt.getTypeFactory().constructParametricType(builderClass, valueType.getBindings()); + } else { + builderType = ctxt.constructType(builderClass); + } BeanDescription builderDesc = ctxt.getConfig().introspectForBuilder(builderType); return buildBuilderBasedDeserializer(ctxt, valueType, builderDesc); } @@ -175,7 +187,27 @@ public class BeanDeserializerFactory } return deser; } - + + /** + * Helper method called to see if given type, otherwise to be taken as POJO type, + * is "known but not supported" JDK type, and if so, return alternate handler + * (deserializer). + * Initially added to support more meaningful error messages when "Java 8 date/time" + * support module not registered. + * + * @since 2.12 + */ + protected JsonDeserializer<Object> _findUnsupportedTypeDeserializer(DeserializationContext ctxt, + JavaType type, BeanDescription beanDesc) + throws JsonMappingException + { + // 05-May-2020, tatu: Should we check for possible Shape override to "POJO"? + // (to let users force 'serialize-as-POJO'? Or not? + final String errorMsg = BeanUtil.checkUnsupportedType(type); + return (errorMsg == null) ? null + : new UnsupportedTypeDeserializer(type, errorMsg); + } + protected JavaType materializeAbstractType(DeserializationContext ctxt, JavaType type, BeanDescription beanDesc) throws JsonMappingException @@ -428,8 +460,7 @@ public class BeanDeserializerFactory /* /********************************************************** - /* Helper methods for Bean deserializer construction, - /* overridable by sub-classes + /* Helper methods for Bean deserializer construction /********************************************************** */ diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BuilderBasedDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/BuilderBasedDeserializer.java index 1ed76ce3b..fa15ed319 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BuilderBasedDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BuilderBasedDeserializer.java @@ -5,6 +5,7 @@ import java.util.*; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.cfg.CoercionAction; import com.fasterxml.jackson.databind.deser.impl.*; import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; import com.fasterxml.jackson.databind.util.NameTransformer; @@ -200,7 +201,7 @@ public class BuilderBasedDeserializer return finishBuild(ctxt, deserializeFromObject(p, ctxt)); } // and then others, generally requiring use of @JsonCreator - switch (p.getCurrentTokenId()) { + switch (p.currentTokenId()) { case JsonTokenId.ID_STRING: return finishBuild(ctxt, deserializeFromString(p, ctxt)); case JsonTokenId.ID_NUMBER_INT: @@ -264,7 +265,7 @@ public class BuilderBasedDeserializer throws IOException { Object bean = _valueInstantiator.createUsingDefault(ctxt); - for (; p.getCurrentToken() == JsonToken.FIELD_NAME; p.nextToken()) { + for (; p.currentToken() == JsonToken.FIELD_NAME; p.nextToken()) { String propName = p.getCurrentName(); // Skip field name: p.nextToken(); @@ -309,7 +310,7 @@ public class BuilderBasedDeserializer return deserializeWithView(p, ctxt, bean, view); } } - for (; p.getCurrentToken() == JsonToken.FIELD_NAME; p.nextToken()) { + for (; p.currentToken() == JsonToken.FIELD_NAME; p.nextToken()) { String propName = p.getCurrentName(); // Skip field name: p.nextToken(); @@ -350,7 +351,7 @@ public class BuilderBasedDeserializer // 04-Jan-2010, tatu: May need to collect unknown properties for polymorphic cases TokenBuffer unknown = null; - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) { String propName = p.getCurrentName(); p.nextToken(); // to point to value @@ -454,7 +455,7 @@ public class BuilderBasedDeserializer return deserializeWithView(p, ctxt, builder, view); } } - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); // 23-Mar-2010, tatu: In some cases, we start with full JSON object too... if (t == JsonToken.START_OBJECT) { t = p.nextToken(); @@ -492,23 +493,29 @@ public class BuilderBasedDeserializer } return finishBuild(ctxt, builder); } - if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { - JsonToken t = p.nextToken(); - if (t == JsonToken.END_ARRAY && ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)) { - return null; - } - final Object value = deserialize(p, ctxt); - if (p.nextToken() != JsonToken.END_ARRAY) { - handleMissingEndArrayForSingle(p, ctxt); - } - return value; - } - if (ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)) { + final CoercionAction act = _findCoercionFromEmptyArray(ctxt); + final boolean unwrap = ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS); + + if (unwrap || (act != CoercionAction.Fail)) { JsonToken t = p.nextToken(); if (t == JsonToken.END_ARRAY) { - return null; + switch (act) { + case AsEmpty: + return getEmptyValue(ctxt); + case AsNull: + case TryConvert: + return getNullValue(ctxt); + default: + } + return ctxt.handleUnexpectedToken(getValueType(ctxt), JsonToken.START_ARRAY, p, null); + } + if (unwrap) { + final Object value = deserialize(p, ctxt); + if (p.nextToken() != JsonToken.END_ARRAY) { + handleMissingEndArrayForSingle(p, ctxt); + } + return value; } - return ctxt.handleUnexpectedToken(getValueType(ctxt), JsonToken.START_ARRAY, p, null); } return ctxt.handleUnexpectedToken(getValueType(ctxt), p); } @@ -523,7 +530,7 @@ public class BuilderBasedDeserializer Object bean, Class<?> activeView) throws IOException { - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) { String propName = p.getCurrentName(); // Skip field name: @@ -575,7 +582,7 @@ public class BuilderBasedDeserializer } final Class<?> activeView = _needViewProcesing ? ctxt.getActiveView() : null; - for (; p.getCurrentToken() == JsonToken.FIELD_NAME; p.nextToken()) { + for (; p.currentToken() == JsonToken.FIELD_NAME; p.nextToken()) { String propName = p.getCurrentName(); p.nextToken(); SettableBeanProperty prop = _beanProperties.find(propName); @@ -625,7 +632,7 @@ public class BuilderBasedDeserializer tokens.writeStartObject(); Object builder = null; - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) { String propName = p.getCurrentName(); p.nextToken(); // to point to value @@ -687,7 +694,7 @@ public class BuilderBasedDeserializer throws IOException { final Class<?> activeView = _needViewProcesing ? ctxt.getActiveView() : null; - for (JsonToken t = p.getCurrentToken(); t == JsonToken.FIELD_NAME; t = p.nextToken()) { + for (JsonToken t = p.currentToken(); t == JsonToken.FIELD_NAME; t = p.nextToken()) { String propName = p.getCurrentName(); SettableBeanProperty prop = _beanProperties.find(propName); p.nextToken(); @@ -742,7 +749,7 @@ public class BuilderBasedDeserializer final Class<?> activeView = _needViewProcesing ? ctxt.getActiveView() : null; final ExternalTypeHandler ext = _externalTypeIdHandler.start(); - for (JsonToken t = p.getCurrentToken(); t == JsonToken.FIELD_NAME; t = p.nextToken()) { + for (JsonToken t = p.currentToken(); t == JsonToken.FIELD_NAME; t = p.nextToken()) { String propName = p.getCurrentName(); t = p.nextToken(); SettableBeanProperty prop = _beanProperties.find(propName); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/DefaultDeserializationContext.java b/src/main/java/com/fasterxml/jackson/databind/deser/DefaultDeserializationContext.java index cdc90ed2e..cf43d1f56 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/DefaultDeserializationContext.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/DefaultDeserializationContext.java @@ -1,5 +1,6 @@ package com.fasterxml.jackson.databind.deser; +import java.io.IOException; import java.util.*; import java.util.Map.Entry; @@ -8,7 +9,7 @@ import com.fasterxml.jackson.annotation.ObjectIdResolver; import com.fasterxml.jackson.annotation.ObjectIdGenerator.IdKey; import com.fasterxml.jackson.core.JsonParser; - +import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; import com.fasterxml.jackson.databind.deser.impl.ReadableObjectId; @@ -43,10 +44,16 @@ public abstract class DefaultDeserializationContext protected DefaultDeserializationContext(DeserializerFactory df, DeserializerCache cache) { super(df, cache); } - + + protected DefaultDeserializationContext(DefaultDeserializationContext src, + DeserializationConfig config, JsonParser p, InjectableValues values) { + super(src, config, p, values); + } + + // @since 2.12 protected DefaultDeserializationContext(DefaultDeserializationContext src, - DeserializationConfig config, JsonParser jp, InjectableValues values) { - super(src, config, jp, values); + DeserializationConfig config) { + super(src, config); } protected DefaultDeserializationContext(DefaultDeserializationContext src, @@ -60,7 +67,7 @@ public abstract class DefaultDeserializationContext protected DefaultDeserializationContext(DefaultDeserializationContext src) { super(src); } - + /** * Method needed to ensure that {@link ObjectMapper#copy} will work * properly; specifically, that caches are cleared, but settings @@ -82,9 +89,8 @@ public abstract class DefaultDeserializationContext @Override public ReadableObjectId findObjectId(Object id, ObjectIdGenerator<?> gen, ObjectIdResolver resolverType) { - /* 02-Apr-2015, tatu: As per [databind#742] should allow 'null', similar to how - * missing id already works. - */ + // 02-Apr-2015, tatu: As per [databind#742] should allow 'null', similar to how + // missing id already works. if (id == null) { return null; } @@ -209,9 +215,8 @@ public abstract class DefaultDeserializationContext if (deserDef instanceof JsonDeserializer) { deser = (JsonDeserializer<?>) deserDef; } else { - /* Alas, there's no way to force return type of "either class - * X or Y" -- need to throw an exception after the fact - */ + // Alas, there's no way to force return type of "either class + // X or Y" -- need to throw an exception after the fact if (!(deserDef instanceof Class)) { throw new IllegalStateException("AnnotationIntrospector returned deserializer definition of type "+deserDef.getClass().getName()+"; expected type JsonDeserializer or Class<JsonDeserializer> instead"); } @@ -280,7 +285,7 @@ public abstract class DefaultDeserializationContext /* /********************************************************** - /* Extended API + /* Extended API, life-cycle /********************************************************** */ @@ -295,10 +300,73 @@ public abstract class DefaultDeserializationContext * context instance. */ public abstract DefaultDeserializationContext createInstance( - DeserializationConfig config, JsonParser jp, InjectableValues values); + DeserializationConfig config, JsonParser p, InjectableValues values); + + public abstract DefaultDeserializationContext createDummyInstance( + DeserializationConfig config); /* /********************************************************** + /* Extended API, read methods + /********************************************************** + */ + + public Object readRootValue(JsonParser p, JavaType valueType, + JsonDeserializer<Object> deser, Object valueToUpdate) + throws IOException + { + if (_config.useRootWrapping()) { + return _unwrapAndDeserialize(p, valueType, deser, valueToUpdate); + } + if (valueToUpdate == null) { + return deser.deserialize(p, this); + } + return deser.deserialize(p, this, valueToUpdate); + } + + protected Object _unwrapAndDeserialize(JsonParser p, + JavaType rootType, JsonDeserializer<Object> deser, + Object valueToUpdate) + throws IOException + { + PropertyName expRootName = _config.findRootName(rootType); + // 12-Jun-2015, tatu: Should try to support namespaces etc but... + String expSimpleName = expRootName.getSimpleName(); + if (p.currentToken() != JsonToken.START_OBJECT) { + reportWrongTokenException(rootType, JsonToken.START_OBJECT, + "Current token not START_OBJECT (needed to unwrap root name '%s'), but %s", + expSimpleName, p.currentToken()); + } + if (p.nextToken() != JsonToken.FIELD_NAME) { + reportWrongTokenException(rootType, JsonToken.FIELD_NAME, + "Current token not FIELD_NAME (to contain expected root name '%s'), but %s", + expSimpleName, p.currentToken()); + } + String actualName = p.getCurrentName(); + if (!expSimpleName.equals(actualName)) { + reportPropertyInputMismatch(rootType, actualName, + "Root name '%s' does not match expected ('%s') for type %s", + actualName, expSimpleName, rootType); + } + // ok, then move to value itself.... + p.nextToken(); + final Object result; + if (valueToUpdate == null) { + result = deser.deserialize(p, this); + } else { + result = deser.deserialize(p, this, valueToUpdate); + } + // and last, verify that we now get matching END_OBJECT + if (p.nextToken() != JsonToken.END_OBJECT) { + reportWrongTokenException(rootType, JsonToken.END_OBJECT, + "Current token not END_OBJECT (to match wrapper object with root name '%s'), but %s", + expSimpleName, p.currentToken()); + } + return result; + } + + /* + /********************************************************** /* And then the concrete implementation class /********************************************************** */ @@ -318,23 +386,27 @@ public abstract class DefaultDeserializationContext super(df, null); } - protected Impl(Impl src, - DeserializationConfig config, JsonParser jp, InjectableValues values) { - super(src, config, jp, values); + private Impl(Impl src, + DeserializationConfig config, JsonParser p, InjectableValues values) { + super(src, config, p, values); } - protected Impl(Impl src) { super(src); } - - protected Impl(Impl src, DeserializerFactory factory) { + private Impl(Impl src) { super(src); } + + private Impl(Impl src, DeserializerFactory factory) { super(src, factory); } + private Impl(Impl src, DeserializationConfig config) { + super(src, config); + } + @Override public DefaultDeserializationContext copy() { ClassUtil.verifyMustOverride(Impl.class, this, "copy"); - return new Impl(this); + return new Impl(this); } - + @Override public DefaultDeserializationContext createInstance(DeserializationConfig config, JsonParser p, InjectableValues values) { @@ -342,8 +414,14 @@ public abstract class DefaultDeserializationContext } @Override + public DefaultDeserializationContext createDummyInstance(DeserializationConfig config) { + // need to be careful to create "real", not blue-print, instance + return new Impl(this, config); + } + + @Override public DefaultDeserializationContext with(DeserializerFactory factory) { return new Impl(this, factory); - } + } } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java b/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java index 9ec8880df..d5c09f38a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java @@ -381,7 +381,7 @@ public final class DeserializerCache // but that won't work for other reasons. So do it here. // (read: rewrite for 3.0) JsonFormat.Value format = beanDesc.findExpectedFormat(null); - if ((format == null) || format.getShape() != JsonFormat.Shape.OBJECT) { + if (format.getShape() != JsonFormat.Shape.OBJECT) { MapLikeType mlt = (MapLikeType) type; if (mlt.isTrueMapType()) { return factory.createMapDeserializer(ctxt,(MapType) mlt, beanDesc); @@ -396,7 +396,7 @@ public final class DeserializerCache * reasons. So do it here. */ JsonFormat.Value format = beanDesc.findExpectedFormat(null); - if ((format == null) || format.getShape() != JsonFormat.Shape.OBJECT) { + if (format.getShape() != JsonFormat.Shape.OBJECT) { CollectionLikeType clt = (CollectionLikeType) type; if (clt.isTrueCollectionType()) { return factory.createCollectionDeserializer(ctxt, (CollectionType) clt, beanDesc); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/ValueInstantiator.java b/src/main/java/com/fasterxml/jackson/databind/deser/ValueInstantiator.java index 701ca6816..572e99bbc 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/ValueInstantiator.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/ValueInstantiator.java @@ -3,9 +3,11 @@ package com.fasterxml.jackson.databind.deser; import java.io.IOException; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.cfg.CoercionAction; +import com.fasterxml.jackson.databind.cfg.CoercionInputShape; import com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer; -import com.fasterxml.jackson.databind.introspect.AnnotatedParameter; import com.fasterxml.jackson.databind.introspect.AnnotatedWithParams; +import com.fasterxml.jackson.databind.type.LogicalType; /** * Class that defines simple API implemented by objects that create value @@ -32,6 +34,45 @@ public abstract class ValueInstantiator { /* /********************************************************** + /* Introspection + /********************************************************** + */ + + /** + * @since 2.9 + */ + public interface Gettable { + public ValueInstantiator getValueInstantiator(); + } + + /* + /********************************************************** + /* Life-cycle + /********************************************************** + */ + + /** + * "Contextualization" method that is called after construction but before first + * use, to allow instantiator access to context needed to possible resolve its + * dependencies. + * + * @param ctxt Currently active deserialization context: needed to (for example) + * resolving {@link com.fasterxml.jackson.databind.jsontype.TypeDeserializer}s. + * + * @return This instance, if no change, or newly constructed instance + * + * @throws JsonMappingException If there are issues with contextualization + * + * @since 2.12 + */ + public ValueInstantiator createContextual(DeserializationContext ctxt, BeanDescription beanDesc) + throws JsonMappingException + { + return this; + } + + /* + /********************************************************** /* Metadata accessors /********************************************************** */ @@ -63,7 +104,7 @@ public abstract class ValueInstantiator } /** - * Method that will return true if any of <code>canCreateXxx</code> method + * Method that will return true if any of {@code canCreateXxx} method * returns true: that is, if there is any way that an instance could * be created. */ @@ -77,7 +118,10 @@ public abstract class ValueInstantiator /** * Method that can be called to check whether a String-based creator - * is available for this instantiator + * is available for this instantiator. + *<p> + * NOTE: does NOT include possible case of fallbacks, or coercion; only + * considers explicit creator. */ public boolean canCreateFromString() { return false; } @@ -249,13 +293,15 @@ public abstract class ValueInstantiator /* /********************************************************** - /* Instantiation methods for JSON scalar types - /* (String, Number, Boolean) + /* Instantiation methods for JSON scalar types (String, Number, Boolean) /********************************************************** */ - + public Object createFromString(DeserializationContext ctxt, String value) throws IOException { - return _createFromStringFallbacks(ctxt, value); + return ctxt.handleMissingInstantiator(getValueClass(), this, ctxt.getParser(), + "no String-argument constructor/factory method to deserialize from String value ('%s')", + value); + } public Object createFromInt(DeserializationContext ctxt, int value) throws IOException { @@ -331,12 +377,6 @@ public abstract class ValueInstantiator */ public AnnotatedWithParams getWithArgsCreator() { return null; } - /** - * If an incomplete creator was found, this is the first parameter that - * needs further annotation to help make the creator complete. - */ - public AnnotatedParameter getIncompleteParameter() { return null; } - /* /********************************************************** /* Helper methods @@ -345,27 +385,34 @@ public abstract class ValueInstantiator /** * @since 2.4 (demoted from <code>StdValueInstantiator</code>) + * @deprecated Since 2.12 should not handle coercions here */ + @Deprecated // since 2.12 protected Object _createFromStringFallbacks(DeserializationContext ctxt, String value) throws IOException { + // also, empty Strings might be accepted as null Object... + if (value.length() == 0) { + if (ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)) { + return null; + } + } + /* 28-Sep-2011, tatu: Ok this is not clean at all; but since there are legacy * systems that expect conversions in some cases, let's just add a minimal * patch (note: same could conceivably be used for numbers too). */ if (canCreateFromBoolean()) { - String str = value.trim(); - if ("true".equals(str)) { - return createFromBoolean(ctxt, true); - } - if ("false".equals(str)) { - return createFromBoolean(ctxt, false); - } - } - // also, empty Strings might be accepted as null Object... - if (value.length() == 0) { - if (ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)) { - return null; + // 29-May-2020, tatu: With 2.12 can and should use CoercionConfig so: + if (ctxt.findCoercionAction(LogicalType.Boolean, Boolean.class, + CoercionInputShape.String) == CoercionAction.TryConvert) { + String str = value.trim(); + if ("true".equals(str)) { + return createFromBoolean(ctxt, true); + } + if ("false".equals(str)) { + return createFromBoolean(ctxt, false); + } } } return ctxt.handleMissingInstantiator(getValueClass(), this, ctxt.getParser(), @@ -375,19 +422,6 @@ public abstract class ValueInstantiator /* /********************************************************** - /* Introspection - /********************************************************** - */ - - /** - * @since 2.9 - */ - public interface Gettable { - public ValueInstantiator getValueInstantiator(); - } - - /* - /********************************************************** /* Standard Base implementation (since 2.8) /********************************************************** */ @@ -421,4 +455,151 @@ public abstract class ValueInstantiator return _valueType; } } + + /** + * Delegating {@link ValueInstantiator} implementation meant as a base type + * that by default delegates methods to specified fallback instantiator. + * + * @since 2.12 + */ + public static class Delegating extends ValueInstantiator + implements java.io.Serializable + { + private static final long serialVersionUID = 1L; + + protected final ValueInstantiator _delegate; + + protected Delegating(ValueInstantiator delegate) { + _delegate = delegate; + } + + @Override + public ValueInstantiator createContextual(DeserializationContext ctxt, BeanDescription beanDesc) + throws JsonMappingException + { + ValueInstantiator d = _delegate.createContextual(ctxt, beanDesc); + return (d == _delegate) ? this : new Delegating(d); + } + + protected ValueInstantiator delegate() { return _delegate; } + + @Override + public Class<?> getValueClass() { return delegate().getValueClass(); } + + @Override + public String getValueTypeDesc() { return delegate().getValueTypeDesc(); } + + @Override + public boolean canInstantiate() { return delegate().canInstantiate(); } + + @Override + public boolean canCreateFromString() { return delegate().canCreateFromString(); } + @Override + public boolean canCreateFromInt() { return delegate().canCreateFromInt(); } + @Override + public boolean canCreateFromLong() { return delegate().canCreateFromLong(); } + @Override + public boolean canCreateFromDouble() { return delegate().canCreateFromDouble(); } + @Override + public boolean canCreateFromBoolean() { return delegate().canCreateFromBoolean(); } + @Override + public boolean canCreateUsingDefault() { return delegate().canCreateUsingDefault(); } + @Override + public boolean canCreateUsingDelegate() { return delegate().canCreateUsingDelegate(); } + @Override + public boolean canCreateUsingArrayDelegate() { return delegate().canCreateUsingArrayDelegate(); } + @Override + public boolean canCreateFromObjectWith() { return delegate().canCreateFromObjectWith(); } + + @Override + public SettableBeanProperty[] getFromObjectArguments(DeserializationConfig config) { + return delegate().getFromObjectArguments(config); + } + + @Override + public JavaType getDelegateType(DeserializationConfig config) { + return delegate().getDelegateType(config); + } + + @Override + public JavaType getArrayDelegateType(DeserializationConfig config) { + return delegate().getArrayDelegateType(config); + } + + /* + /********************************************************** + /* Creation methods + /********************************************************** + */ + + @Override + public Object createUsingDefault(DeserializationContext ctxt) throws IOException { + return delegate().createUsingDefault(ctxt); + } + + @Override + public Object createFromObjectWith(DeserializationContext ctxt, Object[] args) throws IOException { + return delegate().createFromObjectWith(ctxt, args); + } + + @Override + public Object createFromObjectWith(DeserializationContext ctxt, + SettableBeanProperty[] props, PropertyValueBuffer buffer) + throws IOException { + return delegate().createFromObjectWith(ctxt, props, buffer); + } + + @Override + public Object createUsingDelegate(DeserializationContext ctxt, Object delegate) throws IOException { + return delegate().createUsingDelegate(ctxt, delegate); + } + + @Override + public Object createUsingArrayDelegate(DeserializationContext ctxt, Object delegate) throws IOException { + return delegate().createUsingArrayDelegate(ctxt, delegate); + } + + @Override + public Object createFromString(DeserializationContext ctxt, String value) throws IOException { + return delegate().createFromString(ctxt, value); + } + + @Override + public Object createFromInt(DeserializationContext ctxt, int value) throws IOException { + return delegate().createFromInt(ctxt, value); + } + + @Override + public Object createFromLong(DeserializationContext ctxt, long value) throws IOException { + return delegate().createFromLong(ctxt, value); + } + + @Override + public Object createFromDouble(DeserializationContext ctxt, double value) throws IOException { + return delegate().createFromDouble(ctxt, value); + } + + @Override + public Object createFromBoolean(DeserializationContext ctxt, boolean value) throws IOException { + return delegate().createFromBoolean(ctxt, value); + } + + /* + /********************************************************** + /* Accessors for underlying creator objects (optional) + /********************************************************** + */ + + @Override + public AnnotatedWithParams getDefaultCreator() { return delegate().getDefaultCreator(); } + + @Override + public AnnotatedWithParams getDelegateCreator() { return delegate().getDelegateCreator(); } + + @Override + public AnnotatedWithParams getArrayDelegateCreator() { return delegate().getArrayDelegateCreator(); } + + @Override + public AnnotatedWithParams getWithArgsCreator() { return delegate().getWithArgsCreator(); } + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/BeanAsArrayBuilderDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/BeanAsArrayBuilderDeserializer.java index c583d021b..3c9cf8b07 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/BeanAsArrayBuilderDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/BeanAsArrayBuilderDeserializer.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.*; import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.jackson.databind.util.NameTransformer; public class BeanAsArrayBuilderDeserializer @@ -314,7 +315,8 @@ public class BeanAsArrayBuilderDeserializer */ return ctxt.reportBadDefinition(_beanType, String.format( "Cannot support implicit polymorphic deserialization for POJOs-as-Arrays style: nominal type %s, actual type %s", - _beanType.getRawClass().getName(), builder.getClass().getName())); + ClassUtil.getTypeDescription(_beanType), + builder.getClass().getName())); } } continue; @@ -350,7 +352,7 @@ public class BeanAsArrayBuilderDeserializer // Let's start with failure String message = "Cannot deserialize a POJO (of type %s) from non-Array representation (token: %s): " + "type/property designed to be serialized as JSON Array"; - return ctxt.handleUnexpectedToken(getValueType(ctxt), p.getCurrentToken(), p, message, _beanType.getRawClass().getName(), p.getCurrentToken()); + return ctxt.handleUnexpectedToken(getValueType(ctxt), p.currentToken(), p, message, _beanType.getRawClass().getName(), p.currentToken()); // in future, may allow use of "standard" POJO serialization as well; if so, do: //return _delegate.deserialize(p, ctxt); } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/BeanAsArrayDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/BeanAsArrayDeserializer.java index f51899d1e..f0116727b 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/BeanAsArrayDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/BeanAsArrayDeserializer.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.*; +import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.jackson.databind.util.NameTransformer; /** @@ -335,7 +336,8 @@ public class BeanAsArrayDeserializer ctxt.reportBadDefinition(_beanType, String.format( "Cannot support implicit polymorphic deserialization for POJOs-as-Arrays style: " +"nominal type %s, actual type %s", - _beanType.getRawClass().getName(), bean.getClass().getName())); + ClassUtil.getTypeDescription(_beanType), + ClassUtil.getClassDescription(bean))); } } continue; @@ -370,7 +372,8 @@ public class BeanAsArrayDeserializer { String message = "Cannot deserialize a POJO (of type %s) from non-Array representation (token: %s): " +"type/property designed to be serialized as JSON Array"; - return ctxt.handleUnexpectedToken(getValueType(ctxt), p.getCurrentToken(), p, message, _beanType.getRawClass().getName(), p.getCurrentToken()); + return ctxt.handleUnexpectedToken(getValueType(ctxt), p.currentToken(), p, + message, ClassUtil.getTypeDescription(_beanType), p.currentToken()); // in future, may allow use of "standard" POJO serialization as well; if so, do: //return _delegate.deserialize(p, ctxt); } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/BeanPropertyMap.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/BeanPropertyMap.java index 8a3b6f152..68e17ad0b 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/BeanPropertyMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/BeanPropertyMap.java @@ -178,13 +178,6 @@ public class BeanPropertyMap _hashArea[ix+1] = newProp; } - @Deprecated // since 2.8 - public BeanPropertyMap(boolean caseInsensitive, Collection<SettableBeanProperty> props) - { - this(caseInsensitive, props, Collections.<String,List<PropertyName>>emptyMap(), - Locale.getDefault()); - } - /** * @since 2.8 */ @@ -276,8 +269,22 @@ public class BeanPropertyMap } /** + * @since 2.12 + */ + public static BeanPropertyMap construct(MapperConfig<?> config, + Collection<SettableBeanProperty> props, + Map<String,List<PropertyName>> aliasMapping, + boolean caseInsensitive) { + return new BeanPropertyMap(caseInsensitive, + props, aliasMapping, + config.getLocale()); + } + + /** * @since 2.11 + * @deprecated since 2.12 */ + @Deprecated public static BeanPropertyMap construct(MapperConfig<?> config, Collection<SettableBeanProperty> props, Map<String,List<PropertyName>> aliasMapping) { @@ -295,12 +302,6 @@ public class BeanPropertyMap return new BeanPropertyMap(caseInsensitive, props, aliasMapping); } - @Deprecated // since 2.9 - public static BeanPropertyMap construct(Collection<SettableBeanProperty> props, boolean caseInsensitive) { - return construct(props, caseInsensitive, - Collections.<String,List<PropertyName>>emptyMap()); - } - /** * Fluent copy method that creates a new instance that is a copy * of this instance except for one additional property that is @@ -363,7 +364,7 @@ public class BeanPropertyMap } // should we try to re-index? Ordering probably changed but caller probably doesn't want changes... // 26-Feb-2017, tatu: Probably SHOULD handle renaming wrt Aliases? - return new BeanPropertyMap(_caseInsensitive, newProps, _aliasDefs); + return new BeanPropertyMap(_caseInsensitive, newProps, _aliasDefs, _locale); } /* @@ -399,7 +400,7 @@ public class BeanPropertyMap } } // should we try to re-index? Apparently no need - return new BeanPropertyMap(_caseInsensitive, newProps, _aliasDefs); + return new BeanPropertyMap(_caseInsensitive, newProps, _aliasDefs, _locale); } @Deprecated // in 2.9.4 -- must call method that takes old and new property to avoid mismatch diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCollector.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCollector.java index 65706bf6b..193511992 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCollector.java @@ -29,9 +29,10 @@ public class CreatorCollector { protected final static String[] TYPE_DESCS = new String[] { "default", "from-String", "from-int", "from-long", "from-double", - "from-boolean", "delegate", "property-based" }; + "from-boolean", "delegate", "property-based", "array-delegate" + }; - /// Type of bean being created + // Type of bean being created final protected BeanDescription _beanDesc; final protected boolean _canFixAccess; @@ -46,13 +47,13 @@ public class CreatorCollector { * * @since 2.5 */ - protected final AnnotatedWithParams[] _creators = new AnnotatedWithParams[9]; + final protected AnnotatedWithParams[] _creators = new AnnotatedWithParams[9]; /** * Bitmask of creators that were explicitly marked as creators; false for * auto-detected (ones included base on naming and/or visibility, not * annotation) - * + * * @since 2.5 */ protected int _explicitCreators = 0; @@ -296,31 +297,31 @@ public class CreatorCollector { Class<?> newType = newOne.getRawParameterType(0); if (oldType == newType) { - // 13-Jul-2016, tatu: One more thing to check; since Enum - // classes always have - // implicitly created `valueOf()`, let's resolve in favor of - // other implicit - // creator (`fromString()`) + // 13-Jul-2016, tatu: One more thing to check; since Enum classes + // always have implicitly created `valueOf()`, let's resolve in + // favor of other implicit creator (`fromString()`) if (_isEnumValueOf(newOne)) { return false; // ignore } if (_isEnumValueOf(oldOne)) { ; } else { - throw new IllegalArgumentException(String.format( - "Conflicting %s creators: already had %s creator %s, encountered another: %s", - TYPE_DESCS[typeIndex], - explicit ? "explicitly marked" - : "implicitly discovered", - oldOne, newOne)); + _reportDuplicateCreator(typeIndex, explicit, oldOne, newOne); } } // otherwise, which one to choose? else if (newType.isAssignableFrom(oldType)) { // new type more generic, use old return false; + } else if (oldType.isAssignableFrom(newType)) { + // new type more specific, use it + ; + } else { + // 02-May-2020, tatu: Should this only result in exception if both + // explicit? Doing so could lead to arbitrary choice between + // multiple implicit creators tho? + _reportDuplicateCreator(typeIndex, explicit, oldOne, newOne); } - // new type more specific, use it } } if (explicit) { @@ -330,6 +331,17 @@ public class CreatorCollector { return true; } + // @since 2.12 + protected void _reportDuplicateCreator(int typeIndex, boolean explicit, + AnnotatedWithParams oldOne, AnnotatedWithParams newOne) { + throw new IllegalArgumentException(String.format( + "Conflicting %s creators: already had %s creator %s, encountered another: %s", + TYPE_DESCS[typeIndex], + explicit ? "explicitly marked" + : "implicitly discovered", + oldOne, newOne)); + } + /** * Helper method for recognizing `Enum.valueOf()` factory method * diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/ExternalTypeHandler.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/ExternalTypeHandler.java index 516866547..4d956c8f9 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/ExternalTypeHandler.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/ExternalTypeHandler.java @@ -350,9 +350,8 @@ public class ExternalTypeHandler protected final void _deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object bean, int index, String typeId) throws IOException { - /* Ok: time to mix type id, value; and we will actually use "wrapper-array" - * style to ensure we can handle all kinds of JSON constructs. - */ + // Ok: time to mix type id, value; and we will actually use "wrapper-array" + // style to ensure we can handle all kinds of JSON constructs. JsonParser p2 = _tokens[index].asParser(p); JsonToken t = p2.nextToken(); // 29-Sep-2015, tatu: As per [databind#942], nulls need special support diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/FailingDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/FailingDeserializer.java index 9df6742bd..ba9b55299 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/FailingDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/FailingDeserializer.java @@ -4,12 +4,11 @@ import java.io.IOException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; /** * Special bogus "serializer" that will throw - * {@link JsonMappingException} if an attempt is made to deserialize + * {@link com.fasterxml.jackson.databind.exc.MismatchedInputException} if an attempt is made to deserialize * a value. This is used as placeholder to avoid NPEs for uninitialized * structured serializers or handlers. */ @@ -20,7 +19,12 @@ public class FailingDeserializer extends StdDeserializer<Object> protected final String _message; public FailingDeserializer(String m) { - super(Object.class); + this(Object.class, m); + } + + // @since 2.12 + public FailingDeserializer(Class<?> rawType, String m) { + super(rawType); _message = m; } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/TypeWrappedDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/TypeWrappedDeserializer.java index 8d5039de6..c9c291bf7 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/TypeWrappedDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/TypeWrappedDeserializer.java @@ -6,6 +6,7 @@ import java.util.Collection; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; /** * Simple deserializer that will call configured type deserializer, passing @@ -32,6 +33,11 @@ public final class TypeWrappedDeserializer _deserializer = (JsonDeserializer<Object>) deser; } + @Override // since 2.12 + public LogicalType logicalType() { + return _deserializer.logicalType(); + } + @Override public Class<?> handledType() { return _deserializer.handledType(); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/UnsupportedTypeDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/UnsupportedTypeDeserializer.java new file mode 100644 index 000000000..a7a39148b --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/UnsupportedTypeDeserializer.java @@ -0,0 +1,39 @@ +package com.fasterxml.jackson.databind.deser.impl; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +/** + * Special bogus "serializer" that will throw + * {@link com.fasterxml.jackson.databind.exc.MismatchedInputException} + * if an attempt is made to deserialize a value. + * This is used for "known unknown" types: types that we can recognize + * but can not support easily (or support known to be added via extension + * module). + * + * @since 2.12 + */ +public class UnsupportedTypeDeserializer extends StdDeserializer<Object> +{ + private static final long serialVersionUID = 1L; + + protected final JavaType _type; + + protected final String _message; + + public UnsupportedTypeDeserializer(JavaType t, String m) { + super(t); + _type = t; + _message = m; + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + ctxt.reportBadDefinition(_type, _message); + return null; + } +} diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/ArrayBlockingQueueDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/ArrayBlockingQueueDeserializer.java index 94db83214..a27211b9e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/ArrayBlockingQueueDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/ArrayBlockingQueueDeserializer.java @@ -85,18 +85,16 @@ public class ArrayBlockingQueueDeserializer return null; } + // NOTE: implementation changed between 2.11 and 2.12 @Override - public Collection<Object> deserialize(JsonParser p, DeserializationContext ctxt, - Collection<Object> result0) throws IOException + protected Collection<Object> _deserializeFromArray(JsonParser p, DeserializationContext ctxt, + Collection<Object> result0) + throws IOException { - if (result0 != null) { - return super.deserialize(p, ctxt, result0); - } - // Ok: must point to START_ARRAY (or equivalent) - if (!p.isExpectedStartArrayToken()) { - return handleNonArray(p, ctxt, new ArrayBlockingQueue<>(1)); + if (result0 == null) { // usual case + result0 = new ArrayList<>(); } - result0 = super.deserialize(p, ctxt, new ArrayList<>()); + result0 = super._deserializeFromArray(p, ctxt, result0); if (result0.isEmpty()) { return new ArrayBlockingQueue<>(1, false); } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/AtomicBooleanDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/AtomicBooleanDeserializer.java index c8aa43173..8d75c01ef 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/AtomicBooleanDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/AtomicBooleanDeserializer.java @@ -4,7 +4,10 @@ import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.type.LogicalType; public class AtomicBooleanDeserializer extends StdScalarDeserializer<AtomicBoolean> { @@ -14,6 +17,24 @@ public class AtomicBooleanDeserializer extends StdScalarDeserializer<AtomicBoole @Override public AtomicBoolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - return new AtomicBoolean(_parseBooleanPrimitive(ctxt, p, AtomicBoolean.class)); + JsonToken t = p.currentToken(); + if (t == JsonToken.VALUE_TRUE) { + return new AtomicBoolean(true); + } + if (t == JsonToken.VALUE_FALSE) { + return new AtomicBoolean(false); + } + // 12-Jun-2020, tatu: May look convoluted, but need to work correctly with + // CoercionConfig + Boolean b = _parseBoolean(p, ctxt, AtomicBoolean.class); + return (b == null) ? null : new AtomicBoolean(b.booleanValue()); } -}
\ No newline at end of file + + @Override + public LogicalType logicalType() { return LogicalType.Boolean; } + + @Override // @since 2.12 + public Object getEmptyValue(DeserializationContext ctxt) throws JsonMappingException { + return new AtomicBoolean(false); + } +} diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/ByteBufferDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/ByteBufferDeserializer.java index 633a7b8d6..1c2599b03 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/ByteBufferDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/ByteBufferDeserializer.java @@ -5,6 +5,7 @@ import java.nio.ByteBuffer; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.ByteBufferBackedOutputStream; public class ByteBufferDeserializer extends StdScalarDeserializer<ByteBuffer> @@ -13,6 +14,11 @@ public class ByteBufferDeserializer extends StdScalarDeserializer<ByteBuffer> protected ByteBufferDeserializer() { super(ByteBuffer.class); } + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Binary; + } + @Override public ByteBuffer deserialize(JsonParser parser, DeserializationContext cx) throws IOException { byte[] b = parser.getBinaryValue(); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/CollectionDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/CollectionDeserializer.java index 028279265..ce201285b 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/CollectionDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/CollectionDeserializer.java @@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.deser.*; import com.fasterxml.jackson.databind.deser.impl.ReadableObjectId.Referring; import com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.ClassUtil; /** @@ -129,6 +130,11 @@ public class CollectionDeserializer ; } + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Collection; + } + /* /********************************************************** /* Validation, post-processing (ResolvableDeserializer) @@ -232,22 +238,18 @@ _containerType, return (Collection<Object>) _valueInstantiator.createUsingDelegate(ctxt, _delegateDeserializer.deserialize(p, ctxt)); } + // 16-May-2020, tatu: As per [dataformats-text#199] need to first check for + // possible Array-coercion and only after that String coercion + if (p.isExpectedStartArrayToken()) { + return _deserializeFromArray(p, ctxt, createDefaultInstance(ctxt)); + } // Empty String may be ok; bit tricky to check, however, since // there is also possibility of "auto-wrapping" of single-element arrays. // Hence we only accept empty String here. if (p.hasToken(JsonToken.VALUE_STRING)) { - // 16-May-2020, tatu: As [dataformats-text#199] need to avoid blocking - // check to `isExpectedStartArrayToken()` (needed for CSV in-field array/list logic) - // ... alas, trying to do this here leads to 2 unit test regressions so will - // need to figure out safer mechanism. -// if (_valueInstantiator.canCreateFromString()) { - String str = p.getText(); - if (str.length() == 0) { - return (Collection<Object>) _valueInstantiator.createFromString(ctxt, str); -// } - } + return _deserializeFromString(p, ctxt); } - return deserialize(p, ctxt, createDefaultInstance(ctxt)); + return handleNonArray(p, ctxt, createDefaultInstance(ctxt)); } /** @@ -259,16 +261,35 @@ _containerType, { return (Collection<Object>) _valueInstantiator.createUsingDefault(ctxt); } - + @Override public Collection<Object> deserialize(JsonParser p, DeserializationContext ctxt, Collection<Object> result) throws IOException { // Ok: must point to START_ARRAY (or equivalent) - if (!p.isExpectedStartArrayToken()) { - return handleNonArray(p, ctxt, result); + if (p.isExpectedStartArrayToken()) { + return _deserializeFromArray(p, ctxt, result); } + return handleNonArray(p, ctxt, result); + } + + @Override + public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, + TypeDeserializer typeDeserializer) + throws IOException + { + // In future could check current token... for now this should be enough: + return typeDeserializer.deserializeTypedFromArray(p, ctxt); + } + + /** + * @since 2.12 + */ + protected Collection<Object> _deserializeFromArray(JsonParser p, DeserializationContext ctxt, + Collection<Object> result) + throws IOException + { // [databind#631]: Assign current value, to be accessible by custom serializers p.setCurrentValue(result); @@ -310,15 +331,6 @@ _containerType, return result; } - @Override - public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, - TypeDeserializer typeDeserializer) - throws IOException - { - // In future could check current token... for now this should be enough: - return typeDeserializer.deserializeTypedFromArray(p, ctxt); - } - /** * Helper method called when current token is no START_ARRAY. Will either * throw an exception, or try to handle value as if member of implicit diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/ContainerDeserializerBase.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/ContainerDeserializerBase.java index 46c1537da..956ab3b0f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/ContainerDeserializerBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/ContainerDeserializerBase.java @@ -126,14 +126,6 @@ public abstract class ContainerDeserializerBase<T> */ public abstract JsonDeserializer<Object> getContentDeserializer(); - /** - * @since 2.9 - */ - @Override - public ValueInstantiator getValueInstantiator() { - return null; - } - @Override // since 2.9 public AccessPattern getEmptyAccessPattern() { // 02-Feb-2017, tatu: Empty containers are usually constructed as needed diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/DateDeserializers.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/DateDeserializers.java index b416764ff..9af1525b0 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/DateDeserializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/DateDeserializers.java @@ -13,7 +13,9 @@ import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; +import com.fasterxml.jackson.databind.cfg.CoercionAction; import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.jackson.databind.util.StdDateFormat; @@ -101,6 +103,11 @@ public class DateDeserializers protected abstract DateBasedDeserializer<T> withDateFormat(DateFormat df, String formatStr); + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.DateTime; + } + @Override public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) @@ -185,7 +192,15 @@ public class DateDeserializers if (p.hasToken(JsonToken.VALUE_STRING)) { String str = p.getText().trim(); if (str.length() == 0) { - return (Date) getEmptyValue(ctxt); + final CoercionAction act = _checkFromStringCoercion(ctxt, str); + switch (act) { // note: Fail handled above + case AsEmpty: + return new java.util.Date(0L); + case AsNull: + case TryConvert: + default: + } + return null; } synchronized (_customFormat) { try { @@ -239,6 +254,13 @@ public class DateDeserializers return new CalendarDeserializer(this, df, formatString); } + @Override // since 2.12 + public Object getEmptyValue(DeserializationContext ctxt) { + GregorianCalendar cal = new GregorianCalendar(); + cal.setTimeInMillis(0L); + return cal; + } + @Override public Calendar deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { @@ -284,6 +306,11 @@ public class DateDeserializers protected DateDeserializer withDateFormat(DateFormat df, String formatString) { return new DateDeserializer(this, df, formatString); } + + @Override // since 2.12 + public Object getEmptyValue(DeserializationContext ctxt) { + return new Date(0L); + } @Override public java.util.Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { @@ -307,7 +334,12 @@ public class DateDeserializers protected SqlDateDeserializer withDateFormat(DateFormat df, String formatString) { return new SqlDateDeserializer(this, df, formatString); } - + + @Override // since 2.12 + public Object getEmptyValue(DeserializationContext ctxt) { + return new java.sql.Date(0L); + } + @Override public java.sql.Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { Date d = _parseDate(p, ctxt); @@ -333,7 +365,12 @@ public class DateDeserializers protected TimestampDeserializer withDateFormat(DateFormat df, String formatString) { return new TimestampDeserializer(this, df, formatString); } - + + @Override // since 2.12 + public Object getEmptyValue(DeserializationContext ctxt) { + return new Timestamp(0L); + } + @Override public java.sql.Timestamp deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/DelegatingDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/DelegatingDeserializer.java index f4368780e..764e534ce 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/DelegatingDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/DelegatingDeserializer.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.*; import com.fasterxml.jackson.databind.deser.impl.ObjectIdReader; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.AccessPattern; /** @@ -152,6 +153,11 @@ public abstract class DelegatingDeserializer return _delegatee.getEmptyValue(ctxt); } + @Override // since 2.12 + public LogicalType logicalType() { + return _delegatee.logicalType(); + } + @Override public Collection<Object> getKnownPropertyNames() { return _delegatee.getKnownPropertyNames(); } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java index 2018daf15..ae0957251 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.deser.ValueInstantiator; import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.jackson.databind.util.CompactStringObjectMap; import com.fasterxml.jackson.databind.util.EnumResolver; @@ -164,24 +165,23 @@ public class EnumDeserializer @Override public boolean isCachable() { return true; } + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Enum; + } + @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String text; JsonToken curr = p.currentToken(); // Usually should just get string value: if (curr == JsonToken.VALUE_STRING || curr == JsonToken.FIELD_NAME) { - CompactStringObjectMap lookup = ctxt.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING) - ? _getToStringLookup(ctxt) : _lookupByName; - final String name = p.getText(); - Object result = lookup.find(name); - if (result == null) { - return _deserializeAltString(p, ctxt, lookup, name); - } - return result; - } + text = p.getText(); + // But let's consider int acceptable as well (if within ordinal range) - if (curr == JsonToken.VALUE_NUMBER_INT) { + } else if (curr == JsonToken.VALUE_NUMBER_INT) { // ... unless told not to do that int index = p.getIntValue(); if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS)) { @@ -202,8 +202,23 @@ public class EnumDeserializer _enumsByIndex.length-1); } return null; + } else if (curr == JsonToken.START_OBJECT) { + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + text = ctxt.extractScalarFromObject(p, this, _valueClass); + } else { + return _deserializeOther(p, ctxt); + } + + CompactStringObjectMap lookup = ctxt.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING) + ? _getToStringLookup(ctxt) : _lookupByName; + Object result = lookup.find(text); + if (result == null) { + String trimmed = text.trim(); + if ((trimmed == text) || (result = lookup.find(trimmed)) == null) { + return _deserializeAltString(p, ctxt, lookup, trimmed); + } } - return _deserializeOther(p, ctxt); + return result; } /* diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumMapDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumMapDeserializer.java index cfc1133dc..8fde2a371 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumMapDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumMapDeserializer.java @@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.deser.ValueInstantiator; import com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator; import com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.ClassUtil; /** @@ -200,6 +201,11 @@ public class EnumMapDeserializer && (_valueTypeDeserializer == null); } + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Map; + } + /* /********************************************************** /* ContainerDeserializerBase API @@ -211,7 +217,11 @@ public class EnumMapDeserializer return _valueDeserializer; } - // Must override since we do not expose ValueInstantiator + @Override + public ValueInstantiator getValueInstantiator() { + return _valueInstantiator; + } + @Override // since 2.9 public Object getEmptyValue(DeserializationContext ctxt) throws JsonMappingException { return constructMap(ctxt); @@ -234,34 +244,21 @@ public class EnumMapDeserializer return (EnumMap<?,?>) _valueInstantiator.createUsingDelegate(ctxt, _delegateDeserializer.deserialize(p, ctxt)); } - // Ok: must point to START_OBJECT (or similar) + switch (p.currentTokenId()) { case JsonTokenId.ID_START_OBJECT: case JsonTokenId.ID_END_OBJECT: case JsonTokenId.ID_FIELD_NAME: return deserialize(p, ctxt, constructMap(ctxt)); case JsonTokenId.ID_STRING: - return (EnumMap<?,?>) _valueInstantiator.createFromString(ctxt, p.getText()); + // (empty) String may be ok however; or single-String-arg ctor + return _deserializeFromString(p, ctxt); case JsonTokenId.ID_START_ARRAY: - { - JsonToken t = p.nextToken(); - if (t == JsonToken.END_ARRAY) { - if (ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)) { - return null; - } - } else if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { - final Object value = deserialize(p, ctxt); - if (p.nextToken() != JsonToken.END_ARRAY) { - handleMissingEndArrayForSingle(p, ctxt); - } - return (EnumMap<?,?>) value; - } - } - return (EnumMap<?,?>) ctxt.handleUnexpectedToken(getValueType(ctxt), JsonToken.START_ARRAY, p, null); + // Empty array, or single-value wrapped in array? + return _deserializeFromArray(p, ctxt); default: } - // slightly redundant (since String was passed above), but also handles empty array case: - return _deserializeFromEmpty(p, ctxt); + return (EnumMap<?,?>) ctxt.handleUnexpectedToken(getValueType(ctxt), p); } @Override diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumSetDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumSetDeserializer.java index e5286ae78..7b415a5ba 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumSetDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumSetDeserializer.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.deser.NullValueProvider; import com.fasterxml.jackson.databind.deser.impl.NullsConstantProvider; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.AccessPattern; /** @@ -126,7 +127,7 @@ public class EnumSetDeserializer /* Basic metadata /********************************************************** */ - + /** * Because of costs associated with constructing Enum resolvers, * let's cache instances by default. @@ -140,6 +141,11 @@ public class EnumSetDeserializer return true; } + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Collection; + } + @Override // since 2.9 public Boolean supportsUpdate(DeserializationConfig config) { return Boolean.TRUE; @@ -165,6 +171,9 @@ public class EnumSetDeserializer public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { + // 07-May-2020, tatu: Is the argument `EnumSet.class` correct here? + // In a way seems like it should rather refer to value class... ? + // (as it's individual value of element type, not Container)... final Boolean unwrapSingle = findFormatFeature(ctxt, property, EnumSet.class, JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY); JsonDeserializer<?> deser = _enumDeserializer; diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java index 479130923..06488b195 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator; import com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer; import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.ClassUtil; /** @@ -99,11 +100,19 @@ class FactoryBasedEnumDeserializer return Boolean.FALSE; } + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Enum; + } + // since 2.9.7: should have been the case earlier but @Override public boolean isCachable() { return true; } @Override + public ValueInstantiator getValueInstantiator() { return _valueInstantiator; } + + @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { Object value; diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringDeserializer.java index b13383593..52e52832d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringDeserializer.java @@ -15,7 +15,10 @@ import java.util.regex.Pattern; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.core.util.VersionUtil; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.cfg.CoercionAction; +import com.fasterxml.jackson.databind.cfg.CoercionInputShape; import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.ClassUtil; /** @@ -67,13 +70,15 @@ public abstract class FromStringDeserializer<T> extends StdScalarDeserializer<T> TimeZone.class, InetAddress.class, InetSocketAddress.class, + + // Special impl: StringBuilder.class, }; } - + /* /********************************************************** - /* Deserializer implementations + /* Life-cycle /********************************************************** */ @@ -85,7 +90,7 @@ public abstract class FromStringDeserializer<T> extends StdScalarDeserializer<T> * Factory method for trying to find a deserializer for one of supported * types that have simple from-String serialization. */ - public static Std findDeserializer(Class<?> rawType) + public static FromStringDeserializer<?> findDeserializer(Class<?> rawType) { int kind = 0; if (rawType == File.class) { @@ -113,12 +118,17 @@ public abstract class FromStringDeserializer<T> extends StdScalarDeserializer<T> } else if (rawType == InetSocketAddress.class) { kind = Std.STD_INET_SOCKET_ADDRESS; } else if (rawType == StringBuilder.class) { - kind = Std.STD_STRING_BUILDER; + return new StringBuilderDeserializer(); } else { return null; } return new Std(rawType, kind); } + + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.OtherScalar; + } /* /********************************************************** @@ -132,33 +142,49 @@ public abstract class FromStringDeserializer<T> extends StdScalarDeserializer<T> { // Let's get textual value, possibly via coercion from other scalar types String text = p.getValueAsString(); - if (text != null) { // has String representation - if (text.length() == 0 || (text = text.trim()).length() == 0) { - // Usually should become null; but not always - return _deserializeFromEmptyString(); - } - Exception cause = null; - try { - // 19-May-2017, tatu: Used to require non-null result (assuming `null` - // indicated error; but that seems wrong. Should be able to return - // `null` as value. - return _deserialize(text, ctxt); - } catch (IllegalArgumentException | MalformedURLException e) { - cause = e; - } - // note: `cause` can't be null - String msg = "not a valid textual representation"; - String m2 = cause.getMessage(); - if (m2 != null) { - msg = msg + ", problem: "+m2; + if (text == null) { + JsonToken t = p.currentToken(); + if (t != JsonToken.START_OBJECT) { + return (T) _deserializeFromOther(p, ctxt, t); } - // 05-May-2016, tatu: Unlike most usage, this seems legit, so... - JsonMappingException e = ctxt.weirdStringException(text, _valueClass, msg); - e.initCause(cause); - throw e; - // nothing to do here, yet? We'll fail anyway + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + text = ctxt.extractScalarFromObject(p, this, _valueClass); } - JsonToken t = p.currentToken(); + if (text.length() == 0 || (text = text.trim()).length() == 0) { + // 09-Jun-2020, tatu: Commonly `null` but may coerce to "empty" as well + return (T) _deserializeFromEmptyString(ctxt); + } + Exception cause = null; + try { + // 19-May-2017, tatu: Used to require non-null result (assuming `null` + // indicated error; but that seems wrong. Should be able to return + // `null` as value. + return _deserialize(text, ctxt); + } catch (IllegalArgumentException | MalformedURLException e) { + cause = e; + } + // note: `cause` can't be null + String msg = "not a valid textual representation"; + String m2 = cause.getMessage(); + if (m2 != null) { + msg = msg + ", problem: "+m2; + } + // 05-May-2016, tatu: Unlike most usage, this seems legit, so... + JsonMappingException e = ctxt.weirdStringException(text, _valueClass, msg); + e.initCause(cause); + throw e; + } + + /** + * Main method from trying to deserialize actual value from non-empty + * String. + */ + protected abstract T _deserialize(String value, DeserializationContext ctxt) throws IOException; + + // @since 2.12 + protected Object _deserializeFromOther(JsonParser p, DeserializationContext ctxt, + JsonToken t) throws IOException + { // [databind#381] if (t == JsonToken.START_ARRAY) { return _deserializeFromArray(p, ctxt); @@ -170,15 +196,18 @@ public abstract class FromStringDeserializer<T> extends StdScalarDeserializer<T> return null; } if (_valueClass.isAssignableFrom(ob.getClass())) { - return (T) ob; + return ob; } return _deserializeEmbedded(ob, ctxt); } - return (T) ctxt.handleUnexpectedToken(_valueClass, p); + return ctxt.handleUnexpectedToken(_valueClass, p); } - - protected abstract T _deserialize(String value, DeserializationContext ctxt) throws IOException; + /** + * Overridable method to allow coercion from embedded value that is neither + * {@code null} nor directly assignable to target type. + * Used, for example, by {@link UUIDDeserializer} to coerce from {@code byte[]}. + */ protected T _deserializeEmbedded(Object ob, DeserializationContext ctxt) throws IOException { // default impl: error out ctxt.reportInputMismatch(this, @@ -187,10 +216,41 @@ public abstract class FromStringDeserializer<T> extends StdScalarDeserializer<T> return null; } - protected T _deserializeFromEmptyString() throws IOException { + @Deprecated // since 2.12 -- override variant that takes context + protected final T _deserializeFromEmptyString() throws IOException { return null; } + /** + * @since 2.12 + */ + protected Object _deserializeFromEmptyString(DeserializationContext ctxt) throws IOException { + CoercionAction act = ctxt.findCoercionAction(logicalType(), _valueClass, + CoercionInputShape.EmptyString); + if (act == CoercionAction.Fail) { + ctxt.reportInputMismatch(this, +"Cannot coerce empty String (\"\") to %s (but could if enabling coercion using `CoercionConfig`)", +_coercedTypeDesc()); + } + if (act == CoercionAction.AsNull) { + return getNullValue(ctxt); + } + if (act == CoercionAction.AsEmpty) { + return getEmptyValue(ctxt); + } + // 09-Jun-2020, tatu: semantics for `TryConvert` are bit interesting due to + // historical reasons + return _deserializeFromEmptyStringDefault(ctxt); + } + + /** + * @since 2.12 + */ + protected Object _deserializeFromEmptyStringDefault(DeserializationContext ctxt) throws IOException { + // by default, "as-null", but overridable by sub-classes + return getNullValue(ctxt); + } + /* /********************************************************** /* A general-purpose implementation @@ -219,10 +279,11 @@ public abstract class FromStringDeserializer<T> extends StdScalarDeserializer<T> public final static int STD_TIME_ZONE = 10; public final static int STD_INET_ADDRESS = 11; public final static int STD_INET_SOCKET_ADDRESS = 12; - public final static int STD_STRING_BUILDER = 13; + // No longer implemented here since 2.12 + // public final static int STD_STRING_BUILDER = 13; protected final int _kind; - + protected Std(Class<?> valueType, int kind) { super(valueType); _kind = kind; @@ -297,27 +358,32 @@ public abstract class FromStringDeserializer<T> extends StdScalarDeserializer<T> } // host or unbracketed IPv6, without port number return new InetSocketAddress(value, 0); - case STD_STRING_BUILDER: - return new StringBuilder(value); } VersionUtil.throwInternal(); return null; } - @Override - protected Object _deserializeFromEmptyString() throws IOException { - // As per [databind#398], URI requires special handling - if (_kind == STD_URI) { + @Override // since 2.12 + public Object getEmptyValue(DeserializationContext ctxt) + throws JsonMappingException + { + switch (_kind) { + case STD_URI: + // As per [databind#398], URI requires special handling return URI.create(""); - } - // As per [databind#1123], Locale too - if (_kind == STD_LOCALE) { + case STD_LOCALE: + // As per [databind#1123], Locale too return Locale.ROOT; } - if (_kind == STD_STRING_BUILDER) { - return new StringBuilder(); - } - return super._deserializeFromEmptyString(); + return super.getEmptyValue(ctxt); + } + + @Override + protected Object _deserializeFromEmptyStringDefault(DeserializationContext ctxt) throws IOException { + // 09-Jun-2020, tatu: For backwards compatibility deserialize "as-empty" + // as URI and Locale did that in 2.11 (and StringBuilder probably ought to). + // But doing this here instead of super-class lets UUID return "as-null" instead + return getEmptyValue(ctxt); } protected int _firstHyphenOrUnderscore(String str) @@ -331,4 +397,42 @@ public abstract class FromStringDeserializer<T> extends StdScalarDeserializer<T> return -1; } } + + // @since 2.12 to simplify logic a bit: should not use coercions when reading + // String Values + static class StringBuilderDeserializer extends FromStringDeserializer<Object> + { + public StringBuilderDeserializer() { + super(StringBuilder.class); + } + + @Override + public LogicalType logicalType() { + return LogicalType.Textual; + } + + @Override + public Object getEmptyValue(DeserializationContext ctxt) + throws JsonMappingException + { + return new StringBuilder(); + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException + { + String text = p.getValueAsString(); + if (text != null) { + return _deserialize(text, ctxt); + } + return super.deserialize(p, ctxt); + } + + @Override + protected Object _deserialize(String value, DeserializationContext ctxt) + throws IOException + { + return new StringBuilder(value); + } + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/JsonNodeDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/JsonNodeDeserializer.java index fcc4ab82e..26de3c29a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/JsonNodeDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/JsonNodeDeserializer.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.node.*; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.RawValue; /** @@ -184,10 +185,14 @@ abstract class BaseNodeDeserializer<T extends JsonNode> return typeDeserializer.deserializeTypedFromAny(p, ctxt); } - /* 07-Nov-2014, tatu: When investigating [databind#604], realized that it makes - * sense to also mark this is cachable, since lookup not exactly free, and - * since it's not uncommon to "read anything" - */ + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Untyped; + } + + // 07-Nov-2014, tatu: When investigating [databind#604], realized that it makes + // sense to also mark this is cachable, since lookup not exactly free, and + // since it's not uncommon to "read anything" @Override public boolean isCachable() { return true; } @@ -230,6 +235,20 @@ abstract class BaseNodeDeserializer<T extends JsonNode> "Duplicate field '%s' for `ObjectNode`: not allowed when `DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY` enabled", fieldName); } + // [databind#2732]: Special case for XML; automatically coerce into `ArrayNode` + if (ctxt.isEnabled(StreamReadCapability.DUPLICATE_PROPERTIES)) { + // Note that ideally we wouldn't have to shuffle things but... Map.putIfAbsent() + // only added in JDK 8, to efficiently check for add. So... + if (oldValue.isArray()) { // already was array, to append + ((ArrayNode) oldValue).add(newValue); + objectNode.replace(fieldName, oldValue); + } else { // was not array, convert + ArrayNode arr = nodeFactory.arrayNode(); + arr.add(oldValue); + arr.add(newValue); + objectNode.replace(fieldName, arr); + } + } } /* diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/MapDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/MapDeserializer.java index b99f66968..cba73fb4a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/MapDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/MapDeserializer.java @@ -15,10 +15,11 @@ import com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer; import com.fasterxml.jackson.databind.deser.impl.ReadableObjectId.Referring; import com.fasterxml.jackson.databind.introspect.AnnotatedMember; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.ArrayBuilders; /** - * Basic serializer that can take JSON "Object" structure and + * Basic deserializer that can take JSON "Object" structure and * construct a {@link java.util.Map} instance, with typed contents. *<p> * Note: for untyped content (one indicated by passing Object.class @@ -333,6 +334,11 @@ public class MapDeserializer && (_ignorableProperties == null); } + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Map; + } + @Override @SuppressWarnings("unchecked") public Map<Object,Object> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException @@ -349,36 +355,26 @@ public class MapDeserializer getValueInstantiator(), p, "no default constructor found"); } - // Ok: must point to START_OBJECT, FIELD_NAME or END_OBJECT - JsonToken t = p.getCurrentToken(); - if (t != JsonToken.START_OBJECT && t != JsonToken.FIELD_NAME && t != JsonToken.END_OBJECT) { - // (empty) String may be ok however; or single-String-arg ctor - if (t == JsonToken.VALUE_STRING) { - return (Map<Object,Object>) _valueInstantiator.createFromString(ctxt, p.getText()); + switch (p.currentTokenId()) { + case JsonTokenId.ID_START_OBJECT: + case JsonTokenId.ID_END_OBJECT: + case JsonTokenId.ID_FIELD_NAME: + final Map<Object,Object> result = (Map<Object,Object>) _valueInstantiator.createUsingDefault(ctxt); + if (_standardStringKey) { + _readAndBindStringKeyMap(p, ctxt, result); + return result; } - if (t == JsonToken.START_ARRAY) { - if (p.nextToken() == JsonToken.END_ARRAY) { - if (ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)) { - return null; - } - } else if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { - final Object value = deserialize(p, ctxt); - if (p.nextToken() != JsonToken.END_ARRAY) { - handleMissingEndArrayForSingle(p, ctxt); - } - return (Map<Object,Object>) value; - } - // fall through to failing case - } - return (Map<Object,Object>) ctxt.handleUnexpectedToken(getValueType(ctxt), t, p, null); - } - final Map<Object,Object> result = (Map<Object,Object>) _valueInstantiator.createUsingDefault(ctxt); - if (_standardStringKey) { - _readAndBindStringKeyMap(p, ctxt, result); + _readAndBind(p, ctxt, result); return result; + case JsonTokenId.ID_STRING: + // (empty) String may be ok however; or single-String-arg ctor + return _deserializeFromString(p, ctxt); + case JsonTokenId.ID_START_ARRAY: + // Empty array, or single-value wrapped in array? + return _deserializeFromArray(p, ctxt); + default: } - _readAndBind(p, ctxt, result); - return result; + return (Map<Object,Object>) ctxt.handleUnexpectedToken(getValueType(ctxt), p); } @SuppressWarnings("unchecked") @@ -391,7 +387,7 @@ public class MapDeserializer p.setCurrentValue(result); // Ok: must point to START_OBJECT or FIELD_NAME - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t != JsonToken.START_OBJECT && t != JsonToken.FIELD_NAME) { return (Map<Object,Object>) ctxt.handleUnexpectedToken(getMapClass(), p); } @@ -448,7 +444,7 @@ public class MapDeserializer if (p.isExpectedStartObjectToken()) { keyStr = p.nextFieldName(); } else { - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t != JsonToken.FIELD_NAME) { if (t == JsonToken.END_OBJECT) { return; @@ -512,7 +508,7 @@ public class MapDeserializer if (p.isExpectedStartObjectToken()) { key = p.nextFieldName(); } else { - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == JsonToken.END_OBJECT) { return; } @@ -651,7 +647,7 @@ public class MapDeserializer if (p.isExpectedStartObjectToken()) { keyStr = p.nextFieldName(); } else { - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == JsonToken.END_OBJECT) { return; } @@ -720,7 +716,7 @@ public class MapDeserializer if (p.isExpectedStartObjectToken()) { key = p.nextFieldName(); } else { - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == JsonToken.END_OBJECT) { return; } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/MapEntryDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/MapEntryDeserializer.java index 74340131f..2aa0e9adf 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/MapEntryDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/MapEntryDeserializer.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.deser.*; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; /** * Basic serializer that can take JSON "Object" structure and @@ -103,6 +104,12 @@ public class MapEntryDeserializer keyDeser, (JsonDeserializer<Object>) valueDeser, valueTypeDeser); } + @Override // since 2.12 + public LogicalType logicalType() { + // Slightly tricky, could consider POJO too? + return LogicalType.Map; + } + /* /********************************************************** /* Validation, post-processing (ResolvableDeserializer) @@ -155,7 +162,10 @@ public class MapEntryDeserializer public JsonDeserializer<Object> getContentDeserializer() { return _valueDeserializer; } - + + // 31-May-2020, tatu: Should probably define but we don't have it yet +// public ValueInstantiator getValueInstantiator() { } + /* /********************************************************** /* JsonDeserializer API @@ -168,13 +178,14 @@ public class MapEntryDeserializer { // Ok: must point to START_OBJECT, FIELD_NAME or END_OBJECT JsonToken t = p.currentToken(); - if (t != JsonToken.START_OBJECT && t != JsonToken.FIELD_NAME && t != JsonToken.END_OBJECT) { - // String may be ok however: - // slightly redundant (since String was passed above), but - return _deserializeFromEmpty(p, ctxt); - } if (t == JsonToken.START_OBJECT) { t = p.nextToken(); + } else if (t != JsonToken.FIELD_NAME && t != JsonToken.END_OBJECT) { + // Empty array, or single-value wrapped in array? + if (t == JsonToken.START_ARRAY) { + return _deserializeFromArray(p, ctxt); + } + return (Map.Entry<Object,Object>) ctxt.handleUnexpectedToken(getValueType(ctxt), p); } if (t != JsonToken.FIELD_NAME) { if (t == JsonToken.END_OBJECT) { diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java index aa4dad45a..2088fbca2 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java @@ -9,7 +9,10 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.core.io.NumberInput; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; +import com.fasterxml.jackson.databind.cfg.CoercionAction; +import com.fasterxml.jackson.databind.cfg.CoercionInputShape; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.AccessPattern; /** @@ -126,6 +129,9 @@ public class NumberDeserializers { private static final long serialVersionUID = 1L; + // @since 2.12 + protected final LogicalType _logicalType; + protected final T _nullValue; // @since 2.9 @@ -133,13 +139,21 @@ public class NumberDeserializers protected final boolean _primitive; - protected PrimitiveOrWrapperDeserializer(Class<T> vc, T nvl, T empty) { + // @since 2.12 + protected PrimitiveOrWrapperDeserializer(Class<T> vc, LogicalType logicalType, + T nvl, T empty) { super(vc); + _logicalType = logicalType; _nullValue = nvl; _emptyValue = empty; _primitive = vc.isPrimitive(); } + @Deprecated // since 2.12 + protected PrimitiveOrWrapperDeserializer(Class<T> vc, T nvl, T empty) { + this(vc, LogicalType.OtherScalar, nvl, empty); + } + @Override public AccessPattern getNullAccessPattern() { // 02-Feb-2017, tatu: For primitives we must dynamically check (and possibly throw @@ -169,6 +183,11 @@ public class NumberDeserializers public Object getEmptyValue(DeserializationContext ctxt) throws JsonMappingException { return _emptyValue; } + + @Override // since 2.12 + public final LogicalType logicalType() { + return _logicalType; + } } /* @@ -188,20 +207,23 @@ public class NumberDeserializers public BooleanDeserializer(Class<Boolean> cls, Boolean nvl) { - super(cls, nvl, Boolean.FALSE); + super(cls, LogicalType.Boolean, nvl, Boolean.FALSE); } @Override public Boolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == JsonToken.VALUE_TRUE) { return Boolean.TRUE; } if (t == JsonToken.VALUE_FALSE) { return Boolean.FALSE; } - return _parseBoolean(p, ctxt); + if (_primitive) { + return _parseBooleanPrimitive(p, ctxt); + } + return _parseBoolean(p, ctxt, _valueClass); } // Since we can never have type info ("natural type"; String, Boolean, Integer, Double): @@ -211,60 +233,17 @@ public class NumberDeserializers TypeDeserializer typeDeserializer) throws IOException { - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == JsonToken.VALUE_TRUE) { return Boolean.TRUE; } if (t == JsonToken.VALUE_FALSE) { return Boolean.FALSE; } - return _parseBoolean(p, ctxt); - } - - protected final Boolean _parseBoolean(JsonParser p, DeserializationContext ctxt) - throws IOException - { - JsonToken t = p.getCurrentToken(); - if (t == JsonToken.VALUE_NULL) { - return (Boolean) _coerceNullToken(ctxt, _primitive); - } - if (t == JsonToken.START_ARRAY) { // unwrapping? - return _deserializeFromArray(p, ctxt); - } - // should accept ints too, (0 == false, otherwise true) - if (t == JsonToken.VALUE_NUMBER_INT) { - return Boolean.valueOf(_parseBooleanFromInt(p, ctxt)); - } - // And finally, let's allow Strings to be converted too - if (t == JsonToken.VALUE_STRING) { - String text = p.getText().trim(); - // [databind#422]: Allow aliases - if ("true".equals(text) || "True".equals(text)) { - _verifyStringForScalarCoercion(ctxt, text); - return Boolean.TRUE; - } - if ("false".equals(text) || "False".equals(text)) { - _verifyStringForScalarCoercion(ctxt, text); - return Boolean.FALSE; - } - if (text.length() == 0) { - return (Boolean) _coerceEmptyString(ctxt, _primitive); - } - if (_hasTextualNull(text)) { - return (Boolean) _coerceTextualNull(ctxt, _primitive); - } - return (Boolean) ctxt.handleWeirdStringValue(_valueClass, text, - "only \"true\" or \"false\" recognized"); - } - // usually caller should have handled but: - if (t == JsonToken.VALUE_TRUE) { - return Boolean.TRUE; - } - if (t == JsonToken.VALUE_FALSE) { - return Boolean.FALSE; + if (_primitive) { + return _parseBooleanPrimitive(p, ctxt); } - // Otherwise, no can do: - return (Boolean) ctxt.handleUnexpectedToken(_valueClass, p); + return _parseBoolean(p, ctxt, _valueClass); } } @@ -276,67 +255,83 @@ public class NumberDeserializers final static ByteDeserializer primitiveInstance = new ByteDeserializer(Byte.TYPE, (byte) 0); final static ByteDeserializer wrapperInstance = new ByteDeserializer(Byte.class, null); - + public ByteDeserializer(Class<Byte> cls, Byte nvl) { - super(cls, nvl, (byte) 0); + super(cls, LogicalType.Integer, nvl, (byte) 0); } @Override public Byte deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) { + if (p.isExpectedNumberIntToken()) { return p.getByteValue(); } + if (_primitive) { + return _parseBytePrimitive(p, ctxt); + } return _parseByte(p, ctxt); } - protected Byte _parseByte(JsonParser p, DeserializationContext ctxt) throws IOException + protected Byte _parseByte(JsonParser p, DeserializationContext ctxt) + throws IOException { - JsonToken t = p.getCurrentToken(); - if (t == JsonToken.VALUE_STRING) { // let's do implicit re-parse - String text = p.getText().trim(); - if (_hasTextualNull(text)) { - return (Byte) _coerceTextualNull(ctxt, _primitive); - } - int len = text.length(); - if (len == 0) { - return (Byte) _coerceEmptyString(ctxt, _primitive); - } - _verifyStringForScalarCoercion(ctxt, text); - int value; - try { - value = NumberInput.parseInt(text); - } catch (IllegalArgumentException iae) { - return (Byte) ctxt.handleWeirdStringValue(_valueClass, text, - "not a valid Byte value"); + String text; + + switch (p.currentTokenId()) { + case JsonTokenId.ID_STRING: // let's do implicit re-parse + text = p.getText(); + break; + case JsonTokenId.ID_NUMBER_FLOAT: + final CoercionAction act = _checkFloatToIntCoercion(p, ctxt, _valueClass); + if (act == CoercionAction.AsNull) { + return (Byte) getNullValue(ctxt); } - // So far so good: but does it fit? - // as per [JACKSON-804], allow range up to 255, inclusive - if (_byteOverflow(value)) { - return (Byte) ctxt.handleWeirdStringValue(_valueClass, text, - "overflow, value cannot be represented as 8-bit value"); - // fall-through for deferred fails - } - return Byte.valueOf((byte) value); - } - if (t == JsonToken.VALUE_NUMBER_FLOAT) { - if (!ctxt.isEnabled(DeserializationFeature.ACCEPT_FLOAT_AS_INT)) { - _failDoubleToIntCoercion(p, ctxt, "Byte"); + if (act == CoercionAction.AsEmpty) { + return (Byte) getEmptyValue(ctxt); } return p.getByteValue(); + case JsonTokenId.ID_NULL: // null fine for non-primitive + return (Byte) getNullValue(ctxt); + case JsonTokenId.ID_NUMBER_INT: + return p.getByteValue(); + case JsonTokenId.ID_START_ARRAY: + return (Byte) _deserializeFromArray(p, ctxt); + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, _valueClass); + break; + default: + return (Byte) ctxt.handleUnexpectedToken(getValueType(ctxt), p); } - if (t == JsonToken.VALUE_NULL) { - return (Byte) _coerceNullToken(ctxt, _primitive); + + // Rest of the processing is for coercion from String + final CoercionAction act = _checkFromStringCoercion(ctxt, text); + if (act == CoercionAction.AsNull) { + return (Byte) getNullValue(ctxt); } - // [databind#381] - if (t == JsonToken.START_ARRAY) { - return _deserializeFromArray(p, ctxt); + if (act == CoercionAction.AsEmpty) { + return (Byte) getEmptyValue(ctxt); } - if (t == JsonToken.VALUE_NUMBER_INT) { // shouldn't usually be called with it but - return p.getByteValue(); + text = text.trim(); + if (_checkTextualNull(ctxt, text)) { + return (Byte) getNullValue(ctxt); + } + int value; + try { + value = NumberInput.parseInt(text); + } catch (IllegalArgumentException iae) { + return (Byte) ctxt.handleWeirdStringValue(_valueClass, text, + "not a valid Byte value"); } - return (Byte) ctxt.handleUnexpectedToken(_valueClass, p); + // So far so good: but does it fit? + // as per [JACKSON-804], allow range up to 255, inclusive + if (_byteOverflow(value)) { + return (Byte) ctxt.handleWeirdStringValue(_valueClass, text, + "overflow, value cannot be represented as 8-bit value"); + // fall-through for deferred fails + } + return Byte.valueOf((byte) value); } } @@ -351,59 +346,78 @@ public class NumberDeserializers public ShortDeserializer(Class<Short> cls, Short nvl) { - super(cls, nvl, (short)0); + super(cls, LogicalType.Integer, nvl, (short)0); } @Override public Short deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.isExpectedNumberIntToken()) { + return p.getShortValue(); + } + if (_primitive) { + return _parseShortPrimitive(p, ctxt); + } return _parseShort(p, ctxt); } - protected Short _parseShort(JsonParser p, DeserializationContext ctxt) throws IOException + protected Short _parseShort(JsonParser p, DeserializationContext ctxt) + throws IOException { - JsonToken t = p.getCurrentToken(); - if (t == JsonToken.VALUE_NUMBER_INT) { - return p.getShortValue(); - } - if (t == JsonToken.VALUE_STRING) { // let's do implicit re-parse - String text = p.getText().trim(); - int len = text.length(); - if (len == 0) { - return (Short) _coerceEmptyString(ctxt, _primitive); - } - if (_hasTextualNull(text)) { - return (Short) _coerceTextualNull(ctxt, _primitive); - } - _verifyStringForScalarCoercion(ctxt, text); - int value; - try { - value = NumberInput.parseInt(text); - } catch (IllegalArgumentException iae) { - return (Short) ctxt.handleWeirdStringValue(_valueClass, text, - "not a valid Short value"); - } - // So far so good: but does it fit? - if (_shortOverflow(value)) { - return (Short) ctxt.handleWeirdStringValue(_valueClass, text, - "overflow, value cannot be represented as 16-bit value"); + String text; + switch (p.currentTokenId()) { + case JsonTokenId.ID_STRING: // let's do implicit re-parse + text = p.getText(); + break; + case JsonTokenId.ID_NUMBER_FLOAT: + final CoercionAction act = _checkFloatToIntCoercion(p, ctxt, _valueClass); + if (act == CoercionAction.AsNull) { + return (Short) getNullValue(ctxt); } - return Short.valueOf((short) value); - } - if (t == JsonToken.VALUE_NUMBER_FLOAT) { - if (!ctxt.isEnabled(DeserializationFeature.ACCEPT_FLOAT_AS_INT)) { - _failDoubleToIntCoercion(p, ctxt, "Short"); + if (act == CoercionAction.AsEmpty) { + return (Short) getEmptyValue(ctxt); } return p.getShortValue(); + case JsonTokenId.ID_NULL: // null fine for non-primitive + return (Short) getNullValue(ctxt); + case JsonTokenId.ID_NUMBER_INT: + return p.getShortValue(); + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, _valueClass); + break; + case JsonTokenId.ID_START_ARRAY: + return (Short)_deserializeFromArray(p, ctxt); + default: + return (Short) ctxt.handleUnexpectedToken(getValueType(ctxt), p); } - if (t == JsonToken.VALUE_NULL) { - return (Short) _coerceNullToken(ctxt, _primitive); + + // Rest of the processing is for coercion from String + final CoercionAction act = _checkFromStringCoercion(ctxt, text); + if (act == CoercionAction.AsNull) { + return (Short) getNullValue(ctxt); } - if (t == JsonToken.START_ARRAY) { - return _deserializeFromArray(p, ctxt); + if (act == CoercionAction.AsEmpty) { + return (Short) getEmptyValue(ctxt); + } + text = text.trim(); + if (_checkTextualNull(ctxt, text)) { + return (Short) getNullValue(ctxt); + } + int value; + try { + value = NumberInput.parseInt(text); + } catch (IllegalArgumentException iae) { + return (Short) ctxt.handleWeirdStringValue(_valueClass, text, + "not a valid Short value"); } - return (Short) ctxt.handleUnexpectedToken(_valueClass, p); + // So far so good: but does it fit? + if (_shortOverflow(value)) { + return (Short) ctxt.handleWeirdStringValue(_valueClass, text, + "overflow, value cannot be represented as 16-bit value"); + } + return (short) value; } } @@ -418,39 +432,72 @@ public class NumberDeserializers public CharacterDeserializer(Class<Character> cls, Character nvl) { - super(cls, nvl, '\0'); + super(cls, + // 07-Jun-2020, tatu: Debatable if it should be "OtherScalar" or Integer but... + LogicalType.Integer, nvl, '\0'); } @Override public Character deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - switch (p.getCurrentTokenId()) { - case JsonTokenId.ID_NUMBER_INT: // ok iff ascii value - _verifyNumberForScalarCoercion(ctxt, p); + String text; + switch (p.currentTokenId()) { + case JsonTokenId.ID_STRING: + // 23-Jun-2020, tatu: Unlike real numeric types, Character/char does not + // have canonical shape in JSON, and String in particular does not need + // coercion -- as long as it has length of 1. + text = p.getText(); + break; + case JsonTokenId.ID_NUMBER_INT: // ok iff Unicode value + CoercionAction act = ctxt.findCoercionAction(logicalType(), _valueClass, CoercionInputShape.Integer); + switch (act) { + case Fail: + _checkCoercionActionFail(ctxt, act, "Integer value ("+p.getText()+")"); + break; + case AsNull: + return getNullValue(ctxt); + case AsEmpty: + return (Character) getEmptyValue(ctxt); + default: + } int value = p.getIntValue(); if (value >= 0 && value <= 0xFFFF) { return Character.valueOf((char) value); } - break; - case JsonTokenId.ID_STRING: // this is the usual type - // But does it have to be exactly one char? - String text = p.getText(); - if (text.length() == 1) { - return Character.valueOf(text.charAt(0)); - } - // actually, empty should become null? - if (text.length() == 0) { - return (Character) _coerceEmptyString(ctxt, _primitive); + return (Character) ctxt.handleWeirdNumberValue(handledType(), value, + "value outside valid Character range (0x0000 - 0xFFFF)"); + case JsonTokenId.ID_NULL: + if (_primitive) { + _verifyNullForPrimitive(ctxt); } + return (Character) getNullValue(ctxt); + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, _valueClass); break; - case JsonTokenId.ID_NULL: - return (Character) _coerceNullToken(ctxt, _primitive); case JsonTokenId.ID_START_ARRAY: return _deserializeFromArray(p, ctxt); default: + return (Character) ctxt.handleUnexpectedToken(getValueType(ctxt), p); + } + + if (text.length() == 1) { + return Character.valueOf(text.charAt(0)); } - return (Character) ctxt.handleUnexpectedToken(_valueClass, p); + CoercionAction act = _checkFromStringCoercion(ctxt, text); + if (act == CoercionAction.AsNull) { + return getNullValue(ctxt); + } + if (act == CoercionAction.AsEmpty) { + return (Character) getEmptyValue(ctxt); + } + text = text.trim(); + if (_checkTextualNull(ctxt, text)) { + return (Character) getNullValue(ctxt); + } + return (Character) ctxt.handleWeirdStringValue(handledType(), text, + "Expected either Integer value code or 1-character String"); } } @@ -464,7 +511,7 @@ public class NumberDeserializers final static IntegerDeserializer wrapperInstance = new IntegerDeserializer(Integer.class, null); public IntegerDeserializer(Class<Integer> cls, Integer nvl) { - super(cls, nvl, 0); + super(cls, LogicalType.Integer, nvl, 0); } // since 2.6, slightly faster lookups for this very common type @@ -473,9 +520,12 @@ public class NumberDeserializers @Override public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) { + if (p.isExpectedNumberIntToken()) { return p.getIntValue(); } + if (_primitive) { + return _parseIntPrimitive(p, ctxt); + } return _parseInteger(p, ctxt); } @@ -485,55 +535,58 @@ public class NumberDeserializers public Integer deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException { - if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) { + if (p.isExpectedNumberIntToken()) { return p.getIntValue(); } + if (_primitive) { + return _parseIntPrimitive(p, ctxt); + } return _parseInteger(p, ctxt); } - protected final Integer _parseInteger(JsonParser p, DeserializationContext ctxt) throws IOException + protected final Integer _parseInteger(JsonParser p, DeserializationContext ctxt) + throws IOException { - switch (p.getCurrentTokenId()) { - // NOTE: caller assumed to usually check VALUE_NUMBER_INT in fast path - case JsonTokenId.ID_NUMBER_INT: - return Integer.valueOf(p.getIntValue()); + String text; + switch (p.currentTokenId()) { + case JsonTokenId.ID_STRING: + text = p.getText(); + break; case JsonTokenId.ID_NUMBER_FLOAT: // coercing may work too - if (!ctxt.isEnabled(DeserializationFeature.ACCEPT_FLOAT_AS_INT)) { - _failDoubleToIntCoercion(p, ctxt, "Integer"); - } - return Integer.valueOf(p.getValueAsInt()); - case JsonTokenId.ID_STRING: // let's do implicit re-parse - String text = p.getText().trim(); - int len = text.length(); - if (len == 0) { - return (Integer) _coerceEmptyString(ctxt, _primitive); + final CoercionAction act = _checkFloatToIntCoercion(p, ctxt, _valueClass); + if (act == CoercionAction.AsNull) { + return (Integer) getNullValue(ctxt); } - if (_hasTextualNull(text)) { - return (Integer) _coerceTextualNull(ctxt, _primitive); + if (act == CoercionAction.AsEmpty) { + return (Integer) getEmptyValue(ctxt); } - _verifyStringForScalarCoercion(ctxt, text); - try { - if (len > 9) { - long l = Long.parseLong(text); - if (_intOverflow(l)) { - return (Integer) ctxt.handleWeirdStringValue(_valueClass, text, String.format( - "Overflow: numeric value (%s) out of range of Integer (%d - %d)", - text, Integer.MIN_VALUE, Integer.MAX_VALUE)); - } - return Integer.valueOf((int) l); - } - return Integer.valueOf(NumberInput.parseInt(text)); - } catch (IllegalArgumentException iae) { - return (Integer) ctxt.handleWeirdStringValue(_valueClass, text, - "not a valid Integer value"); - } - case JsonTokenId.ID_NULL: - return (Integer) _coerceNullToken(ctxt, _primitive); + return p.getValueAsInt(); + case JsonTokenId.ID_NUMBER_INT: // NOTE: caller assumed to check in fast path + return p.getIntValue(); + case JsonTokenId.ID_NULL: // null fine for non-primitive + return (Integer) getNullValue(ctxt); + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, _valueClass); + break; case JsonTokenId.ID_START_ARRAY: - return _deserializeFromArray(p, ctxt); + return (Integer) _deserializeFromArray(p, ctxt); + default: + return (Integer) ctxt.handleUnexpectedToken(getValueType(ctxt), p); + } + + final CoercionAction act = _checkFromStringCoercion(ctxt, text); + if (act == CoercionAction.AsNull) { + return (Integer) getNullValue(ctxt); + } + if (act == CoercionAction.AsEmpty) { + return (Integer) getEmptyValue(ctxt); } - // Otherwise, no can do: - return (Integer) ctxt.handleUnexpectedToken(_valueClass, p); + text = text.trim(); + if (_checkTextualNull(ctxt, text)) { + return (Integer) getNullValue(ctxt); + } + return _parseIntPrimitive(ctxt, text); } } @@ -547,7 +600,7 @@ public class NumberDeserializers final static LongDeserializer wrapperInstance = new LongDeserializer(Long.class, null); public LongDeserializer(Class<Long> cls, Long nvl) { - super(cls, nvl, 0L); + super(cls, LogicalType.Integer, nvl, 0L); } // since 2.6, slightly faster lookups for this very common type @@ -556,46 +609,59 @@ public class NumberDeserializers @Override public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) { + if (p.isExpectedNumberIntToken()) { return p.getLongValue(); } + if (_primitive) { + return _parseLongPrimitive(p, ctxt); + } return _parseLong(p, ctxt); } - protected final Long _parseLong(JsonParser p, DeserializationContext ctxt) throws IOException + protected final Long _parseLong(JsonParser p, DeserializationContext ctxt) + throws IOException { - switch (p.getCurrentTokenId()) { - // NOTE: caller assumed to usually check VALUE_NUMBER_INT in fast path - case JsonTokenId.ID_NUMBER_INT: - return p.getLongValue(); - case JsonTokenId.ID_NUMBER_FLOAT: - if (!ctxt.isEnabled(DeserializationFeature.ACCEPT_FLOAT_AS_INT)) { - _failDoubleToIntCoercion(p, ctxt, "Long"); - } - return p.getValueAsLong(); + String text; + switch (p.currentTokenId()) { case JsonTokenId.ID_STRING: - String text = p.getText().trim(); - if (text.length() == 0) { - return (Long) _coerceEmptyString(ctxt, _primitive); + text = p.getText(); + break; + case JsonTokenId.ID_NUMBER_FLOAT: + final CoercionAction act = _checkFloatToIntCoercion(p, ctxt, _valueClass); + if (act == CoercionAction.AsNull) { + return (Long) getNullValue(ctxt); } - if (_hasTextualNull(text)) { - return (Long) _coerceTextualNull(ctxt, _primitive); + if (act == CoercionAction.AsEmpty) { + return (Long) getEmptyValue(ctxt); } - _verifyStringForScalarCoercion(ctxt, text); - // let's allow Strings to be converted too - try { - return Long.valueOf(NumberInput.parseLong(text)); - } catch (IllegalArgumentException iae) { } - return (Long) ctxt.handleWeirdStringValue(_valueClass, text, - "not a valid Long value"); - // fall-through - case JsonTokenId.ID_NULL: - return (Long) _coerceNullToken(ctxt, _primitive); + return p.getValueAsLong(); + case JsonTokenId.ID_NULL: // null fine for non-primitive + return (Long) getNullValue(ctxt); + case JsonTokenId.ID_NUMBER_INT: + return p.getLongValue(); + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, _valueClass); + break; case JsonTokenId.ID_START_ARRAY: - return _deserializeFromArray(p, ctxt); + return (Long) _deserializeFromArray(p, ctxt); + default: + return (Long) ctxt.handleUnexpectedToken(getValueType(ctxt), p); + } + + final CoercionAction act = _checkFromStringCoercion(ctxt, text); + if (act == CoercionAction.AsNull) { + return (Long) getNullValue(ctxt); + } + if (act == CoercionAction.AsEmpty) { + return (Long) getEmptyValue(ctxt); } - // Otherwise, no can do: - return (Long) ctxt.handleUnexpectedToken(_valueClass, p); + text = text.trim(); + if (_checkTextualNull(ctxt, text)) { + return (Long) getNullValue(ctxt); + } + // let's allow Strings to be converted too + return _parseLongPrimitive(ctxt, text); } } @@ -609,65 +675,77 @@ public class NumberDeserializers final static FloatDeserializer wrapperInstance = new FloatDeserializer(Float.class, null); public FloatDeserializer(Class<Float> cls, Float nvl) { - super(cls, nvl, 0.f); + super(cls, LogicalType.Float, nvl, 0.f); } @Override public Float deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.hasToken(JsonToken.VALUE_NUMBER_FLOAT)) { + return p.getFloatValue(); + } + if (_primitive) { + return _parseFloatPrimitive(p, ctxt); + } return _parseFloat(p, ctxt); } protected final Float _parseFloat(JsonParser p, DeserializationContext ctxt) throws IOException { - // We accept couple of different types; obvious ones first: - JsonToken t = p.getCurrentToken(); - - if (t == JsonToken.VALUE_NUMBER_FLOAT || t == JsonToken.VALUE_NUMBER_INT) { // coercing should work too + String text; + switch (p.currentTokenId()) { + case JsonTokenId.ID_STRING: + text = p.getText(); + break; + case JsonTokenId.ID_NULL: // null fine for non-primitive + return (Float) getNullValue(ctxt); + case JsonTokenId.ID_NUMBER_FLOAT: + case JsonTokenId.ID_NUMBER_INT: // safe coercion return p.getFloatValue(); + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, _valueClass); + break; + case JsonTokenId.ID_START_ARRAY: + return _deserializeFromArray(p, ctxt); + default: + return (Float) ctxt.handleUnexpectedToken(getValueType(ctxt), p); + } + + final CoercionAction act = _checkFromStringCoercion(ctxt, text); + if (act == CoercionAction.AsNull) { + return (Float) getNullValue(ctxt); + } + if (act == CoercionAction.AsEmpty) { + return (Float) getEmptyValue(ctxt); } - // And finally, let's allow Strings to be converted too - if (t == JsonToken.VALUE_STRING) { - String text = p.getText().trim(); - if ((text.length() == 0)) { - return (Float) _coerceEmptyString(ctxt, _primitive); + text = text.trim(); + if (_checkTextualNull(ctxt, text)) { + return (Float) getNullValue(ctxt); + } + switch (text.charAt(0)) { + case 'I': + if (_isPosInf(text)) { + return Float.POSITIVE_INFINITY; } - if (_hasTextualNull(text)) { - return (Float) _coerceTextualNull(ctxt, _primitive); + break; + case 'N': + if (_isNaN(text)) { + return Float.NaN; } - switch (text.charAt(0)) { - case 'I': - if (_isPosInf(text)) { - return Float.POSITIVE_INFINITY; - } - break; - case 'N': - if (_isNaN(text)) { - return Float.NaN; - } - break; - case '-': - if (_isNegInf(text)) { - return Float.NEGATIVE_INFINITY; - } - break; + break; + case '-': + if (_isNegInf(text)) { + return Float.NEGATIVE_INFINITY; } - _verifyStringForScalarCoercion(ctxt, text); - try { - return Float.parseFloat(text); - } catch (IllegalArgumentException iae) { } - return (Float) ctxt.handleWeirdStringValue(_valueClass, text, - "not a valid Float value"); - } - if (t == JsonToken.VALUE_NULL) { - return (Float) _coerceNullToken(ctxt, _primitive); - } - if (t == JsonToken.START_ARRAY) { - return _deserializeFromArray(p, ctxt); + break; } - // Otherwise, no can do: - return (Float) ctxt.handleUnexpectedToken(_valueClass, p); + try { + return Float.parseFloat(text); + } catch (IllegalArgumentException iae) { } + return (Float) ctxt.handleWeirdStringValue(_valueClass, text, + "not a valid Float value"); } } @@ -681,11 +759,17 @@ public class NumberDeserializers final static DoubleDeserializer wrapperInstance = new DoubleDeserializer(Double.class, null); public DoubleDeserializer(Class<Double> cls, Double nvl) { - super(cls, nvl, 0.d); + super(cls, LogicalType.Float, nvl, 0.d); } @Override public Double deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.hasToken(JsonToken.VALUE_NUMBER_FLOAT)) { + return p.getDoubleValue(); + } + if (_primitive) { + return _parseDoublePrimitive(p, ctxt); + } return _parseDouble(p, ctxt); } @@ -695,55 +779,71 @@ public class NumberDeserializers public Double deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException { + if (p.hasToken(JsonToken.VALUE_NUMBER_FLOAT)) { + return p.getDoubleValue(); + } + if (_primitive) { + return _parseDoublePrimitive(p, ctxt); + } return _parseDouble(p, ctxt); } protected final Double _parseDouble(JsonParser p, DeserializationContext ctxt) throws IOException { - JsonToken t = p.getCurrentToken(); - if (t == JsonToken.VALUE_NUMBER_INT || t == JsonToken.VALUE_NUMBER_FLOAT) { // coercing should work too + String text; + switch (p.currentTokenId()) { + case JsonTokenId.ID_STRING: + text = p.getText(); + break; + case JsonTokenId.ID_NULL: // null fine for non-primitive + return (Double) getNullValue(ctxt); + case JsonTokenId.ID_NUMBER_FLOAT: + case JsonTokenId.ID_NUMBER_INT: // safe coercion return p.getDoubleValue(); + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, _valueClass); + break; + case JsonTokenId.ID_START_ARRAY: + return _deserializeFromArray(p, ctxt); + default: + return (Double) ctxt.handleUnexpectedToken(getValueType(ctxt), p); + } + + // Coercion from String most complicated + final CoercionAction act = _checkFromStringCoercion(ctxt, text); + if (act == CoercionAction.AsNull) { + return (Double) getNullValue(ctxt); + } + if (act == CoercionAction.AsEmpty) { + return (Double) getEmptyValue(ctxt); } - if (t == JsonToken.VALUE_STRING) { - String text = p.getText().trim(); - if ((text.length() == 0)) { - return (Double) _coerceEmptyString(ctxt, _primitive); + text = text.trim(); + if (_checkTextualNull(ctxt, text)) { + return (Double) getNullValue(ctxt); + } + switch (text.charAt(0)) { + case 'I': + if (_isPosInf(text)) { + return Double.POSITIVE_INFINITY; } - if (_hasTextualNull(text)) { - return (Double) _coerceTextualNull(ctxt, _primitive); + break; + case 'N': + if (_isNaN(text)) { + return Double.NaN; } - switch (text.charAt(0)) { - case 'I': - if (_isPosInf(text)) { - return Double.POSITIVE_INFINITY; - } - break; - case 'N': - if (_isNaN(text)) { - return Double.NaN; - } - break; - case '-': - if (_isNegInf(text)) { - return Double.NEGATIVE_INFINITY; - } - break; + break; + case '-': + if (_isNegInf(text)) { + return Double.NEGATIVE_INFINITY; } - _verifyStringForScalarCoercion(ctxt, text); - try { - return parseDouble(text); - } catch (IllegalArgumentException iae) { } - return (Double) ctxt.handleWeirdStringValue(_valueClass, text, - "not a valid Double value"); - } - if (t == JsonToken.VALUE_NULL) { - return (Double) _coerceNullToken(ctxt, _primitive); - } - if (t == JsonToken.START_ARRAY) { - return _deserializeFromArray(p, ctxt); + break; } - // Otherwise, no can do: - return (Double) ctxt.handleUnexpectedToken(_valueClass, p); + try { + return _parseDouble(text); + } catch (IllegalArgumentException iae) { } + return (Double) ctxt.handleWeirdStringValue(_valueClass, text, + "not a valid Double value"); } } @@ -768,10 +868,20 @@ public class NumberDeserializers super(Number.class); } + @Override // since 2.12 + public final LogicalType logicalType() { + // 07-Jun-2020, tatu: Hmmh... tricky choice. For now, use: + return LogicalType.Integer; + } + @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - switch (p.getCurrentTokenId()) { + String text; + switch (p.currentTokenId()) { + case JsonTokenId.ID_STRING: + text = p.getText(); + break; case JsonTokenId.ID_NUMBER_INT: if (ctxt.hasSomeOfFeatures(F_MASK_INT_COERCIONS)) { return _coerceIntegral(p, ctxt); @@ -786,56 +896,60 @@ public class NumberDeserializers } } return p.getNumberValue(); + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, _valueClass); + break; + case JsonTokenId.ID_START_ARRAY: + return _deserializeFromArray(p, ctxt); + default: + return ctxt.handleUnexpectedToken(getValueType(ctxt), p); + } - case JsonTokenId.ID_STRING: - /* Textual values are more difficult... not parsing itself, but figuring - * out 'minimal' type to use - */ - String text = p.getText().trim(); - if ((text.length() == 0)) { - // note: no need to call `coerce` as this is never primitive - return getNullValue(ctxt); - } - if (_hasTextualNull(text)) { - // note: no need to call `coerce` as this is never primitive - return getNullValue(ctxt); - } - if (_isPosInf(text)) { - return Double.POSITIVE_INFINITY; - } - if (_isNegInf(text)) { - return Double.NEGATIVE_INFINITY; + // Textual values are more difficult... not parsing itself, but figuring + // out 'minimal' type to use + CoercionAction act = _checkFromStringCoercion(ctxt, text); + if (act == CoercionAction.AsNull) { + return getNullValue(ctxt); + } + if (act == CoercionAction.AsEmpty) { + return getEmptyValue(ctxt); + } + text = text.trim(); + if (_hasTextualNull(text)) { + // note: no need to call `coerce` as this is never primitive + return getNullValue(ctxt); + } + if (_isPosInf(text)) { + return Double.POSITIVE_INFINITY; + } + if (_isNegInf(text)) { + return Double.NEGATIVE_INFINITY; + } + if (_isNaN(text)) { + return Double.NaN; + } + try { + if (!_isIntNumber(text)) { + if (ctxt.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)) { + return new BigDecimal(text); + } + return Double.valueOf(text); } - if (_isNaN(text)) { - return Double.NaN; + if (ctxt.isEnabled(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS)) { + return new BigInteger(text); } - _verifyStringForScalarCoercion(ctxt, text); - try { - if (!_isIntNumber(text)) { - if (ctxt.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)) { - return new BigDecimal(text); - } - return Double.valueOf(text); - } - if (ctxt.isEnabled(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS)) { - return new BigInteger(text); - } - long value = Long.parseLong(text); - if (!ctxt.isEnabled(DeserializationFeature.USE_LONG_FOR_INTS)) { - if (value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE) { - return Integer.valueOf((int) value); - } + long value = Long.parseLong(text); + if (!ctxt.isEnabled(DeserializationFeature.USE_LONG_FOR_INTS)) { + if (value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE) { + return Integer.valueOf((int) value); } - return Long.valueOf(value); - } catch (IllegalArgumentException iae) { - return ctxt.handleWeirdStringValue(_valueClass, text, - "not a valid number"); } - case JsonTokenId.ID_START_ARRAY: - return _deserializeFromArray(p, ctxt); + return Long.valueOf(value); + } catch (IllegalArgumentException iae) { + return ctxt.handleWeirdStringValue(_valueClass, text, + "not a valid number"); } - // Otherwise, no can do: - return ctxt.handleUnexpectedToken(_valueClass, p); } /** @@ -849,7 +963,7 @@ public class NumberDeserializers TypeDeserializer typeDeserializer) throws IOException { - switch (p.getCurrentTokenId()) { + switch (p.currentTokenId()) { case JsonTokenId.ID_NUMBER_INT: case JsonTokenId.ID_NUMBER_FLOAT: case JsonTokenId.ID_STRING: @@ -862,8 +976,7 @@ public class NumberDeserializers /* /********************************************************** - /* And then bit more complicated (but non-structured) number - /* types + /* And then bit more complicated (but non-structured) number types /********************************************************** */ @@ -885,45 +998,63 @@ public class NumberDeserializers return BigInteger.ZERO; } - @SuppressWarnings("incomplete-switch") + @Override // since 2.12 + public final LogicalType logicalType() { + return LogicalType.Integer; + } + @Override public BigInteger deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - switch (p.getCurrentTokenId()) { - case JsonTokenId.ID_NUMBER_INT: - switch (p.getNumberType()) { - case INT: - case LONG: - case BIG_INTEGER: - return p.getBigIntegerValue(); - } + if (p.isExpectedNumberIntToken()) { + return p.getBigIntegerValue(); + } + + String text; + switch (p.currentTokenId()) { + case JsonTokenId.ID_STRING: // let's do implicit re-parse + text = p.getText(); break; case JsonTokenId.ID_NUMBER_FLOAT: - if (!ctxt.isEnabled(DeserializationFeature.ACCEPT_FLOAT_AS_INT)) { - _failDoubleToIntCoercion(p, ctxt, "java.math.BigInteger"); + final CoercionAction act = _checkFloatToIntCoercion(p, ctxt, _valueClass); + if (act == CoercionAction.AsNull) { + return (BigInteger) getNullValue(ctxt); + } + if (act == CoercionAction.AsEmpty) { + return (BigInteger) getEmptyValue(ctxt); } return p.getDecimalValue().toBigInteger(); + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, _valueClass); + break; case JsonTokenId.ID_START_ARRAY: return _deserializeFromArray(p, ctxt); - case JsonTokenId.ID_STRING: // let's do implicit re-parse - String text = p.getText().trim(); + default: + // String is ok too, can easily convert; otherwise, no can do: + return (BigInteger) ctxt.handleUnexpectedToken(getValueType(ctxt), p); + } + + final CoercionAction act = _checkFromStringCoercion(ctxt, text); + if (act == CoercionAction.AsNull) { + return getNullValue(ctxt); + } + if (act == CoercionAction.AsEmpty) { + return (BigInteger) getEmptyValue(ctxt); + } + text = text.trim(); + if (_hasTextualNull(text)) { // note: no need to call `coerce` as this is never primitive - if (_isEmptyOrTextualNull(text)) { - _verifyNullForScalarCoercion(ctxt, text); - return getNullValue(ctxt); - } - _verifyStringForScalarCoercion(ctxt, text); - try { - return new BigInteger(text); - } catch (IllegalArgumentException iae) { } - return (BigInteger) ctxt.handleWeirdStringValue(_valueClass, text, - "not a valid representation"); + return getNullValue(ctxt); } - // String is ok too, can easily convert; otherwise, no can do: - return (BigInteger) ctxt.handleUnexpectedToken(_valueClass, p); + try { + return new BigInteger(text); + } catch (IllegalArgumentException iae) { } + return (BigInteger) ctxt.handleWeirdStringValue(_valueClass, text, + "not a valid representation"); } } - + @SuppressWarnings("serial") @JacksonStdImpl public static class BigDecimalDeserializer @@ -937,33 +1068,51 @@ public class NumberDeserializers public Object getEmptyValue(DeserializationContext ctxt) { return BigDecimal.ZERO; } - + + @Override // since 2.12 + public final LogicalType logicalType() { + return LogicalType.Float; + } + @Override public BigDecimal deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - switch (p.getCurrentTokenId()) { + String text; + switch (p.currentTokenId()) { case JsonTokenId.ID_NUMBER_INT: case JsonTokenId.ID_NUMBER_FLOAT: return p.getDecimalValue(); case JsonTokenId.ID_STRING: - String text = p.getText().trim(); - // note: no need to call `coerce` as this is never primitive - if (_isEmptyOrTextualNull(text)) { - _verifyNullForScalarCoercion(ctxt, text); - return getNullValue(ctxt); - } - _verifyStringForScalarCoercion(ctxt, text); - try { - return new BigDecimal(text); - } catch (IllegalArgumentException iae) { } - return (BigDecimal) ctxt.handleWeirdStringValue(_valueClass, text, - "not a valid representation"); + text = p.getText(); + break; + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, _valueClass); + break; case JsonTokenId.ID_START_ARRAY: return _deserializeFromArray(p, ctxt); + default: + return (BigDecimal) ctxt.handleUnexpectedToken(getValueType(ctxt), p); + } + + final CoercionAction act = _checkFromStringCoercion(ctxt, text); + if (act == CoercionAction.AsNull) { + return getNullValue(ctxt); + } + if (act == CoercionAction.AsEmpty) { + return (BigDecimal) getEmptyValue(ctxt); + } + text = text.trim(); + if (_hasTextualNull(text)) { + // note: no need to call `coerce` as this is never primitive + return getNullValue(ctxt); } - // Otherwise, no can do: - return (BigDecimal) ctxt.handleUnexpectedToken(_valueClass, p); + try { + return new BigDecimal(text); + } catch (IllegalArgumentException iae) { } + return (BigDecimal) ctxt.handleWeirdStringValue(_valueClass, text, + "not a valid representation"); } } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/ObjectArrayDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/ObjectArrayDeserializer.java index 09980b153..6225f53ff 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/ObjectArrayDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/ObjectArrayDeserializer.java @@ -12,6 +12,8 @@ import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.deser.NullValueProvider; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.ArrayType; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.AccessPattern; import com.fasterxml.jackson.databind.util.ObjectBuffer; @@ -25,8 +27,6 @@ public class ObjectArrayDeserializer { private static final long serialVersionUID = 1L; - protected final static Object[] NO_OBJECTS = new Object[0]; - // // Configuration /** @@ -52,20 +52,27 @@ public class ObjectArrayDeserializer */ protected final TypeDeserializer _elementTypeDeserializer; + /** + * @since 2.12 + */ + protected final Object[] _emptyValue; + /* /********************************************************** /* Life-cycle /********************************************************** */ - public ObjectArrayDeserializer(JavaType arrayType, + public ObjectArrayDeserializer(JavaType arrayType0, JsonDeserializer<Object> elemDeser, TypeDeserializer elemTypeDeser) { - super(arrayType, null, null); + super(arrayType0, null, null); + ArrayType arrayType = (ArrayType) arrayType0; _elementClass = arrayType.getContentType().getRawClass(); _untyped = (_elementClass == Object.class); _elementDeserializer = elemDeser; _elementTypeDeserializer = elemTypeDeser; + _emptyValue = arrayType.getEmptyArray(); } protected ObjectArrayDeserializer(ObjectArrayDeserializer base, @@ -75,6 +82,7 @@ public class ObjectArrayDeserializer super(base, nuller, unwrapSingle); _elementClass = base._elementClass; _untyped = base._untyped; + _emptyValue = base._emptyValue; _elementDeserializer = elemDeser; _elementTypeDeserializer = elemTypeDeser; @@ -114,11 +122,19 @@ public class ObjectArrayDeserializer return (_elementDeserializer == null) && (_elementTypeDeserializer == null); } + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Array; + } + @Override public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { JsonDeserializer<?> valueDeser = _elementDeserializer; + // 07-May-2020, tatu: Is the argument `containerType.getRawClass()` right here? + // In a way seems like it should rather refer to value class... ? + // (as it's individual value of element type, not Container)... Boolean unwrapSingle = findFormatFeature(ctxt, property, _containerType.getRawClass(), JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY); // May have a content converter @@ -157,7 +173,9 @@ public class ObjectArrayDeserializer // need to override as we can't expose ValueInstantiator @Override // since 2.9 public Object getEmptyValue(DeserializationContext ctxt) throws JsonMappingException { - return NO_OBJECTS; + // 03-Jul-2020, tatu: Must be assignment-compatible; can not just return `new Object[0]` + // if element type is different + return _emptyValue; } /* @@ -306,34 +324,29 @@ public class ObjectArrayDeserializer protected Object[] handleNonArray(JsonParser p, DeserializationContext ctxt) throws IOException { - // Empty String can become null... - if (p.hasToken(JsonToken.VALUE_STRING) - && ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)) { - String str = p.getText(); - if (str.length() == 0) { - return null; - } - } - // Can we do implicit coercion to a single-element array still? boolean canWrap = (_unwrapSingle == Boolean.TRUE) || ((_unwrapSingle == null) && ctxt.isEnabled(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)); if (!canWrap) { - // One exception; byte arrays are generally serialized as base64, so that should be handled - if (p.hasToken(JsonToken.VALUE_STRING) - // note: not `byte[]`, but `Byte[]` -- former is primitive array - && _elementClass == Byte.class) { - return deserializeFromBase64(p, ctxt); + // 2 exceptions with Strings: + if (p.hasToken(JsonToken.VALUE_STRING)) { + // One exception; byte arrays are generally serialized as base64, so that should be handled + // note: not `byte[]`, but `Byte[]` -- former is primitive array + if (_elementClass == Byte.class) { + return deserializeFromBase64(p, ctxt); + } + // Second: empty (and maybe blank) String + return _deserializeFromString(p, ctxt); } - return (Object[]) ctxt.handleUnexpectedToken(_containerType.getRawClass(), p); + return (Object[]) ctxt.handleUnexpectedToken(_containerType, p); } Object value; if (p.hasToken(JsonToken.VALUE_NULL)) { // 03-Feb-2017, tatu: Should this be skipped or not? if (_skipNullValues) { - return NO_OBJECTS; + return _emptyValue; } value = _nullProvider.getNullValue(ctxt); } else if (_elementTypeDeserializer == null) { diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/PrimitiveArrayDeserializers.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/PrimitiveArrayDeserializers.java index 2ca116aac..8665bec3f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/PrimitiveArrayDeserializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/PrimitiveArrayDeserializers.java @@ -15,6 +15,7 @@ import com.fasterxml.jackson.databind.deser.impl.NullsConstantProvider; import com.fasterxml.jackson.databind.deser.impl.NullsFailProvider; import com.fasterxml.jackson.databind.exc.InvalidNullException; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.AccessPattern; import com.fasterxml.jackson.databind.util.ArrayBuilders; @@ -151,6 +152,11 @@ public abstract class PrimitiveArrayDeserializers<T> extends StdDeserializer<T> /* Default implementations /******************************************************** */ + + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Array; + } @Override // since 2.9 public Boolean supportsUpdate(DeserializationConfig config) { @@ -205,11 +211,8 @@ public abstract class PrimitiveArrayDeserializers<T> extends StdDeserializer<T> protected T handleNonArray(JsonParser p, DeserializationContext ctxt) throws IOException { // Empty String can become null... - if (p.hasToken(JsonToken.VALUE_STRING) - && ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)) { - if (p.getText().length() == 0) { - return null; - } + if (p.hasToken(JsonToken.VALUE_STRING)) { + return _deserializeFromString(p, ctxt); } boolean canWrap = (_unwrapSingle == Boolean.TRUE) || ((_unwrapSingle == null) && @@ -389,7 +392,7 @@ public abstract class PrimitiveArrayDeserializers<T> extends StdDeserializer<T> _verifyNullForPrimitive(ctxt); value = false; } else { - value = _parseBooleanPrimitive(ctxt, p, Boolean.TYPE); + value = _parseBooleanPrimitive(p, ctxt); } if (ix >= chunk.length) { chunk = builder.appendCompletedChunk(chunk, ix); @@ -406,7 +409,7 @@ public abstract class PrimitiveArrayDeserializers<T> extends StdDeserializer<T> @Override protected boolean[] handleSingleElementUnwrapped(JsonParser p, DeserializationContext ctxt) throws IOException { - return new boolean[] { _parseBooleanPrimitive(ctxt, p, Boolean.TYPE) }; + return new boolean[] { _parseBooleanPrimitive(p, ctxt) }; } @Override @@ -445,6 +448,13 @@ public abstract class PrimitiveArrayDeserializers<T> extends StdDeserializer<T> return new byte[0]; } + @Override // since 2.12 + public LogicalType logicalType() { + // 30-May-2020, tatu: while technically an array, logically contains + // binary data so... + return LogicalType.Binary; + } + @Override public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { @@ -485,9 +495,8 @@ public abstract class PrimitiveArrayDeserializers<T> extends StdDeserializer<T> while ((t = p.nextToken()) != JsonToken.END_ARRAY) { // whether we should allow truncating conversions? byte value; - if (t == JsonToken.VALUE_NUMBER_INT || t == JsonToken.VALUE_NUMBER_FLOAT) { - // should we catch overflow exceptions? - value = p.getByteValue(); + if (t == JsonToken.VALUE_NUMBER_INT) { + value = p.getByteValue(); // note: may throw due to overflow } else { // should probably accept nulls as 0 if (t == JsonToken.VALUE_NULL) { @@ -519,9 +528,8 @@ public abstract class PrimitiveArrayDeserializers<T> extends StdDeserializer<T> { byte value; JsonToken t = p.currentToken(); - if (t == JsonToken.VALUE_NUMBER_INT || t == JsonToken.VALUE_NUMBER_FLOAT) { - // should we catch overflow exceptions? - value = p.getByteValue(); + if (t == JsonToken.VALUE_NUMBER_INT) { + value = p.getByteValue(); // note: may throw due to overflow } else { // should probably accept nulls as 'false' if (t == JsonToken.VALUE_NULL) { diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/ReferenceTypeDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/ReferenceTypeDeserializer.java index 8c96af3fd..5672c75f6 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/ReferenceTypeDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/ReferenceTypeDeserializer.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.deser.ValueInstantiator; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.type.ReferenceType; import com.fasterxml.jackson.databind.util.AccessPattern; @@ -145,7 +146,7 @@ public abstract class ReferenceTypeDeserializer<T> * @since 2.9 */ public abstract Object getReferenced(T reference); - + /* /********************************************************** /* Overridden accessors @@ -153,8 +154,19 @@ public abstract class ReferenceTypeDeserializer<T> */ @Override + public ValueInstantiator getValueInstantiator() { return _valueInstantiator; } + + @Override public JavaType getValueType() { return _fullType; } + @Override // since 2.12 + public LogicalType logicalType() { + if (_valueDeserializer != null) { + return _valueDeserializer.logicalType(); + } + return super.logicalType(); + } + /** * By default we assume that updateability mostly relies on value * deserializer; if it supports updates, typically that's what diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StackTraceElementDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StackTraceElementDeserializer.java index a7b6022ea..55532feb9 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StackTraceElementDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StackTraceElementDeserializer.java @@ -28,7 +28,7 @@ public class StackTraceElementDeserializer int lineNumber = -1; while ((t = p.nextValue()) != JsonToken.END_OBJECT) { - String propName = p.getCurrentName(); + String propName = p.currentName(); // TODO: with Java 8, convert to switch if ("className".equals(propName)) { className = p.getText(); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDelegatingDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDelegatingDeserializer.java index 0b7e0ed08..12ef990db 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDelegatingDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDelegatingDeserializer.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.jackson.databind.util.Converter; @@ -152,6 +153,11 @@ public class StdDelegatingDeserializer<T> return _delegateDeserializer.handledType(); } + @Override // since 2.12 + public LogicalType logicalType() { + return _delegateDeserializer.logicalType(); + } + @Override // since 2.9 public Boolean supportsUpdate(DeserializationConfig config) { return _delegateDeserializer.supportsUpdate(config); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java index 0b36403c1..a3255d3a1 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdDeserializer.java @@ -5,11 +5,16 @@ import java.util.*; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.Nulls; + import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.core.JsonParser.NumberType; import com.fasterxml.jackson.core.exc.InputCoercionException; import com.fasterxml.jackson.core.io.NumberInput; + import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; +import com.fasterxml.jackson.databind.cfg.CoercionAction; +import com.fasterxml.jackson.databind.cfg.CoercionInputShape; import com.fasterxml.jackson.databind.deser.BeanDeserializerBase; import com.fasterxml.jackson.databind.deser.NullValueProvider; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; @@ -19,6 +24,7 @@ import com.fasterxml.jackson.databind.deser.impl.NullsConstantProvider; import com.fasterxml.jackson.databind.deser.impl.NullsFailProvider; import com.fasterxml.jackson.databind.introspect.AnnotatedMember; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.AccessPattern; import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.jackson.databind.util.Converter; @@ -30,7 +36,8 @@ import com.fasterxml.jackson.databind.util.Converter; */ public abstract class StdDeserializer<T> extends JsonDeserializer<T> - implements java.io.Serializable + implements java.io.Serializable, + ValueInstantiator.Gettable // since 2.12 { private static final long serialVersionUID = 1L; @@ -45,7 +52,7 @@ public abstract class StdDeserializer<T> DeserializationFeature.USE_BIG_INTEGER_FOR_INTS.getMask() | DeserializationFeature.USE_LONG_FOR_INTS.getMask(); - // @since 2.9 + @Deprecated // since 2.12 protected final static int F_MASK_ACCEPT_ARRAYS = DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS.getMask() | DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT.getMask(); @@ -90,7 +97,7 @@ public abstract class StdDeserializer<T> @Override public Class<?> handledType() { return _valueClass; } - + /* /********************************************************** /* Extended API @@ -129,6 +136,12 @@ public abstract class StdDeserializer<T> } /** + * @since 2.12 + */ + @Override // for ValueInstantiator.Gettable + public ValueInstantiator getValueInstantiator() { return null; } + + /** * Method that can be called to determine if given deserializer is the default * deserializer Jackson uses; as opposed to a custom deserializer installed by * a module or calling application. Determination is done using @@ -141,7 +154,7 @@ public abstract class StdDeserializer<T> protected boolean isDefaultKeyDeserializer(KeyDeserializer keyDeser) { return ClassUtil.isJacksonStdImpl(keyDeser); } - + /* /********************************************************** /* Partial JsonDeserializer implementation @@ -160,6 +173,194 @@ public abstract class StdDeserializer<T> } /* + /********************************************************************** + /* High-level handling of secondary input shapes (with possible coercion) + /********************************************************************** + */ + + /** + * Helper method that allows easy support for array-related coercion features: + * checks for either empty array, or single-value array-wrapped value (if coercion + * enabled by {@code CoercionConfigs} (since 2.12), and either reports + * an exception (if no coercion allowed), or returns appropriate + * result value using coercion mechanism indicated. + *<p> + * This method should NOT be called if Array representation is explicitly supported + * for type: it should only be called in case it is otherwise unrecognized. + *<p> + * NOTE: in case of unwrapped single element, will handle actual decoding + * by calling {@link #_deserializeWrappedValue}, which by default calls + * {@link #deserialize(JsonParser, DeserializationContext)}. + * + * @since 2.9 + */ + @SuppressWarnings("unchecked") + protected T _deserializeFromArray(JsonParser p, DeserializationContext ctxt) throws IOException + { + final CoercionAction act = _findCoercionFromEmptyArray(ctxt); + final boolean unwrap = ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS); + + if (unwrap || (act != CoercionAction.Fail)) { + JsonToken t = p.nextToken(); + if (t == JsonToken.END_ARRAY) { + switch (act) { + case AsEmpty: + return (T) getEmptyValue(ctxt); + case AsNull: + case TryConvert: + return getNullValue(ctxt); + default: + } + } else if (unwrap) { + final T parsed = _deserializeWrappedValue(p, ctxt); + if (p.nextToken() != JsonToken.END_ARRAY) { + handleMissingEndArrayForSingle(p, ctxt); + } + return parsed; + } + } + return (T) ctxt.handleUnexpectedToken(getValueType(ctxt), JsonToken.START_ARRAY, p, null); + } + + /** + * Helper method that may be used to support fallback for Empty String / Empty Array + * non-standard representations; usually for things serialized as JSON Objects. + * + * @since 2.5 + * + * @deprecated Since 2.12 + */ + @SuppressWarnings("unchecked") + @Deprecated // since 2.12 + protected T _deserializeFromEmpty(JsonParser p, DeserializationContext ctxt) + throws IOException + { + if (p.hasToken(JsonToken.START_ARRAY)) { + if (ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)) { + JsonToken t = p.nextToken(); + if (t == JsonToken.END_ARRAY) { + return null; + } + return (T) ctxt.handleUnexpectedToken(getValueType(ctxt), p); + } + } + return (T) ctxt.handleUnexpectedToken(getValueType(ctxt), p); + } + + /** + * Helper method to call in case deserializer does not support native automatic + * use of incoming String values, but there may be standard coercions to consider. + * + * @since 2.12 + */ + @SuppressWarnings("unchecked") + protected T _deserializeFromString(JsonParser p, DeserializationContext ctxt) + throws IOException + { + final ValueInstantiator inst = getValueInstantiator(); + final Class<?> rawTargetType = handledType(); + String value = p.getValueAsString(); + + if ((inst != null) && inst.canCreateFromString()) { + return (T) inst.createFromString(ctxt, value); + } + + if (value.length() == 0) { + final CoercionAction act = ctxt.findCoercionAction(logicalType(), rawTargetType, + CoercionInputShape.EmptyString); + return (T) _deserializeFromEmptyString(p, ctxt, act, rawTargetType, + "empty String (\"\")"); + } + if (_isBlank(value)) { + final CoercionAction act = ctxt.findCoercionFromBlankString(logicalType(), rawTargetType, + CoercionAction.Fail); + return (T) _deserializeFromEmptyString(p, ctxt, act, rawTargetType, + "blank String (all whitespace)"); + } + + // 28-Sep-2011, tatu: Ok this is not clean at all; but since there are legacy + // systems that expect conversions in some cases, let's just add a minimal + // patch (note: same could conceivably be used for numbers too). + if (inst != null) { + value = value.trim(); // mostly to avoid problems wrt XML indentation + if (inst.canCreateFromInt()) { + if (ctxt.findCoercionAction(LogicalType.Integer, Integer.class, + CoercionInputShape.String) == CoercionAction.TryConvert) { + return (T) inst.createFromInt(ctxt, _parseIntPrimitive(ctxt, value)); + } + } + if (inst.canCreateFromLong()) { + if (ctxt.findCoercionAction(LogicalType.Integer, Long.class, + CoercionInputShape.String) == CoercionAction.TryConvert) { + return (T) inst.createFromLong(ctxt, _parseLongPrimitive(ctxt, value)); + } + } + if (inst.canCreateFromBoolean()) { + // 29-May-2020, tatu: With 2.12 can and should use CoercionConfig so: + if (ctxt.findCoercionAction(LogicalType.Boolean, Boolean.class, + CoercionInputShape.String) == CoercionAction.TryConvert) { + String str = value.trim(); + if ("true".equals(str)) { + return (T) inst.createFromBoolean(ctxt, true); + } + if ("false".equals(str)) { + return (T) inst.createFromBoolean(ctxt, false); + } + } + } + } + return (T) ctxt.handleMissingInstantiator(rawTargetType, inst, ctxt.getParser(), + "no String-argument constructor/factory method to deserialize from String value ('%s')", + value); + } + + protected Object _deserializeFromEmptyString(JsonParser p, DeserializationContext ctxt, + CoercionAction act, Class<?> rawTargetType, String desc) throws IOException + { + switch (act) { + case AsEmpty: + return getEmptyValue(ctxt); + case TryConvert: + // hmmmh... empty or null, typically? Assume "as null" for now + case AsNull: + return null; + case Fail: + break; + } + final ValueInstantiator inst = getValueInstantiator(); + + // 03-Jun-2020, tatu: Should ideally call `handleUnexpectedToken()` instead, but + // since this call was already made, use it. + return ctxt.handleMissingInstantiator(rawTargetType, inst, p, +"Cannot deserialize value of type %s from %s (no String-argument constructor/factory method; coercion not enabled)", + ClassUtil.getTypeDescription(getValueType(ctxt)), desc); + } + + /** + * Helper called to support {@link DeserializationFeature#UNWRAP_SINGLE_VALUE_ARRAYS}: + * default implementation simply calls + * {@link #deserialize(JsonParser, DeserializationContext)}, + * but handling may be overridden. + * + * @since 2.9 + */ + protected T _deserializeWrappedValue(JsonParser p, DeserializationContext ctxt) throws IOException + { + // 23-Mar-2017, tatu: Let's specifically block recursive resolution to avoid + // either supporting nested arrays, or to cause infinite looping. + if (p.hasToken(JsonToken.START_ARRAY)) { + String msg = String.format( +"Cannot deserialize instance of %s out of %s token: nested Arrays not allowed with %s", + ClassUtil.nameOf(_valueClass), JsonToken.START_ARRAY, + "DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS"); + @SuppressWarnings("unchecked") + T result = (T) ctxt.handleUnexpectedToken(getValueType(ctxt), p.currentToken(), p, msg); + return result; + } + return (T) deserialize(p, ctxt); + } + + /* /********************************************************** /* Helper methods for sub-classes, parsing: while mostly /* useful for numeric types, can be also useful for dealing @@ -167,78 +368,251 @@ public abstract class StdDeserializer<T> /********************************************************** */ - @Deprecated // since 2.11, use overloaded variant - protected final boolean _parseBooleanPrimitive(JsonParser p, DeserializationContext ctxt) throws IOException { - return _parseBooleanPrimitive(ctxt, p, Boolean.TYPE); + @Deprecated // since 2.12, use overloaded variant that does NOT take target type + protected final boolean _parseBooleanPrimitive(DeserializationContext ctxt, + JsonParser p, Class<?> targetType) throws IOException { + return _parseBooleanPrimitive(p, ctxt); } - // @since 2.11 - protected final boolean _parseBooleanPrimitive(DeserializationContext ctxt, - JsonParser p, Class<?> targetType) throws IOException + /** + * @param ctxt Deserialization context for accessing configuration + * @param p Underlying parser + * @param targetType Actual type that is being deserialized, typically + * same as {@link #handledType}, and not necessarily {@code boolean} + * (may be {@code boolean[]} or {@code AtomicBoolean} for example); + * used for coercion config access + */ + protected final boolean _parseBooleanPrimitive(JsonParser p, DeserializationContext ctxt) + throws IOException { - JsonToken t = p.getCurrentToken(); - if (t == JsonToken.VALUE_TRUE) return true; - if (t == JsonToken.VALUE_FALSE) return false; - if (t == JsonToken.VALUE_NULL) { + String text; + switch (p.currentTokenId()) { + case JsonTokenId.ID_STRING: + text = p.getText(); + break; + case JsonTokenId.ID_NUMBER_INT: + // may accept ints too, (0 == false, otherwise true) + + // call returns `null`, Boolean.TRUE or Boolean.FALSE so: + return _coerceBooleanFromInt(p, ctxt, Boolean.TYPE) == Boolean.TRUE; + case JsonTokenId.ID_TRUE: // usually caller should have handled but: + return true; + case JsonTokenId.ID_FALSE: + return false; + case JsonTokenId.ID_NULL: // null fine for non-primitive _verifyNullForPrimitive(ctxt); return false; + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, Boolean.TYPE); + break; + case JsonTokenId.ID_START_ARRAY: + // 12-Jun-2020, tatu: For some reason calling `_deserializeFromArray()` won't work so: + if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { + p.nextToken(); + final boolean parsed = _parseBooleanPrimitive(p, ctxt); + _verifyEndArrayForSingle(p, ctxt); + return parsed; + } + // fall through + default: + return ((Boolean) ctxt.handleUnexpectedToken(Boolean.TYPE, p)).booleanValue(); } - // should accept ints too, (0 == false, otherwise true) - if (t == JsonToken.VALUE_NUMBER_INT) { - return _parseBooleanFromInt(p, ctxt); + final CoercionAction act = _checkFromStringCoercion(ctxt, text, + LogicalType.Boolean, Boolean.TYPE); + if (act == CoercionAction.AsNull) { + _verifyNullForPrimitive(ctxt); + return false; + } + if (act == CoercionAction.AsEmpty) { + return false; } - // And finally, let's allow Strings to be converted too - if (t == JsonToken.VALUE_STRING) { - String text = p.getText().trim(); - // [databind#422]: Allow aliases - if ("true".equals(text) || "True".equals(text)) { + text = text.trim(); + final int len = text.length(); + + // For [databind#1852] allow some case-insensitive matches (namely, + // true/True/TRUE, false/False/FALSE + if (len == 4) { + if (_isTrue(text)) { return true; } - if ("false".equals(text) || "False".equals(text)) { - return false; - } - if (_isEmptyOrTextualNull(text)) { - _verifyNullForPrimitiveCoercion(ctxt, text); + } else if (len == 5) { + if (_isFalse(text)) { return false; } - Boolean b = (Boolean) ctxt.handleWeirdStringValue(targetType, text, - "only \"true\" or \"false\" recognized"); - return Boolean.TRUE.equals(b); } - // [databind#381] - if (t == JsonToken.START_ARRAY && ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { - p.nextToken(); - final boolean parsed = _parseBooleanPrimitive(ctxt, p, targetType); - _verifyEndArrayForSingle(p, ctxt); - return parsed; + if (_hasTextualNull(text)) { + _verifyNullForPrimitiveCoercion(ctxt, text); + return false; } - // Otherwise, no can do: - return ((Boolean) ctxt.handleUnexpectedToken(targetType, p)).booleanValue(); + Boolean b = (Boolean) ctxt.handleWeirdStringValue(Boolean.TYPE, text, + "only \"true\"/\"True\"/\"TRUE\" or \"false\"/\"False\"/\"FALSE\" recognized"); + return Boolean.TRUE.equals(b); } - protected boolean _parseBooleanFromInt(JsonParser p, DeserializationContext ctxt) + // [databind#1852] + protected boolean _isTrue(String text) { + char c = text.charAt(0); + if (c == 't') { + return "true".equals(text); + } + if (c == 'T') { + return "TRUE".equals(text) || "True".equals(text); + } + return false; + } + + protected boolean _isFalse(String text) { + char c = text.charAt(0); + if (c == 'f') { + return "false".equals(text); + } + if (c == 'F') { + return "FALSE".equals(text) || "False".equals(text); + } + return false; + } + + /** + * Helper method called for cases where non-primitive, boolean-based value + * is to be deserialized: result of this method will be {@link java.lang.Boolean}, + * although actual target type may be something different. + *<p> + * Note: does NOT dynamically access "empty value" or "null value" of deserializer + * since those values could be of type other than {@link java.lang.Boolean}. + * Caller may need to translate from 3 possible result types into appropriately + * matching output types. + * + * @param p Underlying parser + * @param ctxt Deserialization context for accessing configuration + * @param targetType Actual type that is being deserialized, may be + * same as {@link #handledType} but could be {@code AtomicBoolean} for example. + * Used for coercion config access. + * + * @since 2.12 + */ + protected final Boolean _parseBoolean(JsonParser p, DeserializationContext ctxt, + Class<?> targetType) throws IOException { - // 13-Oct-2016, tatu: As per [databind#1324], need to be careful wrt - // degenerate case of huge integers, legal in JSON. - // ... this is, on the other hand, probably wrong/sub-optimal for non-JSON - // input. For now, no rea - _verifyNumberForScalarCoercion(ctxt, p); - // Anyway, note that since we know it's valid (JSON) integer, it can't have - // extra whitespace to trim. - return !"0".equals(p.getText()); + String text; + switch (p.currentTokenId()) { + case JsonTokenId.ID_STRING: + text = p.getText(); + break; + case JsonTokenId.ID_NUMBER_INT: + // may accept ints too, (0 == false, otherwise true) + return _coerceBooleanFromInt(p, ctxt, targetType); + case JsonTokenId.ID_TRUE: + return true; + case JsonTokenId.ID_FALSE: + return false; + case JsonTokenId.ID_NULL: // null fine for non-primitive + return null; + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, targetType); + break; + case JsonTokenId.ID_START_ARRAY: // unwrapping / from-empty-array coercion? + return (Boolean) _deserializeFromArray(p, ctxt); + default: + return (Boolean) ctxt.handleUnexpectedToken(targetType, p); + } + + final CoercionAction act = _checkFromStringCoercion(ctxt, text, + LogicalType.Boolean, targetType); + if (act == CoercionAction.AsNull) { + return null; + } + if (act == CoercionAction.AsEmpty) { + return false; + } + text = text.trim(); + final int len = text.length(); + + // For [databind#1852] allow some case-insensitive matches (namely, + // true/True/TRUE, false/False/FALSE + if (len == 4) { + if (_isTrue(text)) { + return true; + } + } else if (len == 5) { + if (_isFalse(text)) { + return false; + } + } + if (_checkTextualNull(ctxt, text)) { + return null; + } + return (Boolean) ctxt.handleWeirdStringValue(targetType, text, + "only \"true\" or \"false\" recognized"); } protected final byte _parseBytePrimitive(JsonParser p, DeserializationContext ctxt) throws IOException { - int value = _parseIntPrimitive(p, ctxt); - // So far so good: but does it fit? + String text; + switch (p.currentTokenId()) { + case JsonTokenId.ID_STRING: + text = p.getText(); + break; + case JsonTokenId.ID_NUMBER_FLOAT: + CoercionAction act = _checkFloatToIntCoercion(p, ctxt, Byte.TYPE); + if (act == CoercionAction.AsNull) { + return (byte) 0; + } + if (act == CoercionAction.AsEmpty) { + return (byte) 0; + } + return p.getByteValue(); + case JsonTokenId.ID_NUMBER_INT: + return p.getByteValue(); + case JsonTokenId.ID_NULL: + _verifyNullForPrimitive(ctxt); + return (byte) 0; + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, Byte.TYPE); + break; + case JsonTokenId.ID_START_ARRAY: // unwrapping / from-empty-array coercion? + // 12-Jun-2020, tatu: For some reason calling `_deserializeFromArray()` won't work so: + if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { + p.nextToken(); + final byte parsed = _parseBytePrimitive(p, ctxt); + _verifyEndArrayForSingle(p, ctxt); + return parsed; + } + // fall through + default: + return ((Byte) ctxt.handleUnexpectedToken(ctxt.constructType(Byte.TYPE), p)).byteValue(); + } + + // Coercion from String + CoercionAction act = _checkFromStringCoercion(ctxt, text, + LogicalType.Integer, Byte.TYPE); + if (act == CoercionAction.AsNull) { + return (byte) 0; // no need to check as does not come from `null`, explicit coercion + } + if (act == CoercionAction.AsEmpty) { + return (byte) 0; + } + text = text.trim(); + if (_hasTextualNull(text)) { + _verifyNullForPrimitiveCoercion(ctxt, text); + return (byte) 0; + } + int value; + try { + value = NumberInput.parseInt(text); + } catch (IllegalArgumentException iae) { + return (Byte) ctxt.handleWeirdStringValue(_valueClass, text, + "not a valid `byte` value"); + } + // So far so good: but does it fit? Allow both -128 / 255 range (inclusive) if (_byteOverflow(value)) { - Number v = (Number) ctxt.handleWeirdStringValue(_valueClass, String.valueOf(value), + return (Byte) ctxt.handleWeirdStringValue(_valueClass, text, "overflow, value cannot be represented as 8-bit value"); - return _nonNullNumber(v).byteValue(); } return (byte) value; } @@ -246,12 +620,65 @@ public abstract class StdDeserializer<T> protected final short _parseShortPrimitive(JsonParser p, DeserializationContext ctxt) throws IOException { - int value = _parseIntPrimitive(p, ctxt); - // So far so good: but does it fit? + String text; + switch (p.currentTokenId()) { + case JsonTokenId.ID_STRING: + text = p.getText(); + break; + case JsonTokenId.ID_NUMBER_FLOAT: + CoercionAction act = _checkFloatToIntCoercion(p, ctxt, Short.TYPE); + if (act == CoercionAction.AsNull) { + return (short) 0; + } + if (act == CoercionAction.AsEmpty) { + return (short) 0; + } + return p.getShortValue(); + case JsonTokenId.ID_NUMBER_INT: + return p.getShortValue(); + case JsonTokenId.ID_NULL: + _verifyNullForPrimitive(ctxt); + return (short) 0; + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, Short.TYPE); + break; + case JsonTokenId.ID_START_ARRAY: + // 12-Jun-2020, tatu: For some reason calling `_deserializeFromArray()` won't work so: + if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { + p.nextToken(); + final short parsed = _parseShortPrimitive(p, ctxt); + _verifyEndArrayForSingle(p, ctxt); + return parsed; + } + // fall through to fail + default: + return ((Short) ctxt.handleUnexpectedToken(ctxt.constructType(Short.TYPE), p)).shortValue(); + } + + CoercionAction act = _checkFromStringCoercion(ctxt, text, + LogicalType.Integer, Short.TYPE); + if (act == CoercionAction.AsNull) { + return (short) 0; // no need to check as does not come from `null`, explicit coercion + } + if (act == CoercionAction.AsEmpty) { + return (short) 0; + } + text = text.trim(); + if (_hasTextualNull(text)) { + _verifyNullForPrimitiveCoercion(ctxt, text); + return (short) 0; + } + int value; + try { + value = NumberInput.parseInt(text); + } catch (IllegalArgumentException iae) { + return (Short) ctxt.handleWeirdStringValue(Short.TYPE, text, + "not a valid `short` value"); + } if (_shortOverflow(value)) { - Number v = (Number) ctxt.handleWeirdStringValue(_valueClass, String.valueOf(value), + return (Short) ctxt.handleWeirdStringValue(Short.TYPE, text, "overflow, value cannot be represented as 16-bit value"); - return _nonNullNumber(v).shortValue(); } return (short) value; } @@ -259,25 +686,29 @@ public abstract class StdDeserializer<T> protected final int _parseIntPrimitive(JsonParser p, DeserializationContext ctxt) throws IOException { - if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) { - return p.getIntValue(); - } - switch (p.getCurrentTokenId()) { + String text; + switch (p.currentTokenId()) { case JsonTokenId.ID_STRING: - String text = p.getText().trim(); - if (_isEmptyOrTextualNull(text)) { - _verifyNullForPrimitiveCoercion(ctxt, text); + text = p.getText(); + break; + case JsonTokenId.ID_NUMBER_FLOAT: + final CoercionAction act = _checkFloatToIntCoercion(p, ctxt, Integer.TYPE); + if (act == CoercionAction.AsNull) { return 0; } - return _parseIntPrimitive(ctxt, text); - case JsonTokenId.ID_NUMBER_FLOAT: - if (!ctxt.isEnabled(DeserializationFeature.ACCEPT_FLOAT_AS_INT)) { - _failDoubleToIntCoercion(p, ctxt, "int"); + if (act == CoercionAction.AsEmpty) { + return 0; } return p.getValueAsInt(); + case JsonTokenId.ID_NUMBER_INT: + return p.getIntValue(); case JsonTokenId.ID_NULL: _verifyNullForPrimitive(ctxt); return 0; + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, Integer.TYPE); + break; case JsonTokenId.ID_START_ARRAY: if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { p.nextToken(); @@ -285,11 +716,25 @@ public abstract class StdDeserializer<T> _verifyEndArrayForSingle(p, ctxt); return parsed; } - break; + // fall through to fail default: + return ((Number) ctxt.handleUnexpectedToken(Integer.TYPE, p)).intValue(); } - // Otherwise, no can do: - return ((Number) ctxt.handleUnexpectedToken(_valueClass, p)).intValue(); + + final CoercionAction act = _checkFromStringCoercion(ctxt, text, + LogicalType.Integer, Integer.TYPE); + if (act == CoercionAction.AsNull) { + return 0; // no need to check as does not come from `null`, explicit coercion + } + if (act == CoercionAction.AsEmpty) { + return 0; + } + text = text.trim(); + if (_hasTextualNull(text)) { + _verifyNullForPrimitiveCoercion(ctxt, text); + return 0; + } + return _parseIntPrimitive(ctxt, text); } /** @@ -301,7 +746,7 @@ public abstract class StdDeserializer<T> if (text.length() > 9) { long l = Long.parseLong(text); if (_intOverflow(l)) { - Number v = (Number) ctxt.handleWeirdStringValue(_valueClass, text, + Number v = (Number) ctxt.handleWeirdStringValue(Integer.TYPE, text, "Overflow: numeric value (%s) out of range of int (%d -%d)", text, Integer.MIN_VALUE, Integer.MAX_VALUE); return _nonNullNumber(v).intValue(); @@ -310,34 +755,38 @@ public abstract class StdDeserializer<T> } return NumberInput.parseInt(text); } catch (IllegalArgumentException iae) { - Number v = (Number) ctxt.handleWeirdStringValue(_valueClass, text, - "not a valid int value"); + Number v = (Number) ctxt.handleWeirdStringValue(Integer.TYPE, text, + "not a valid `int` value"); return _nonNullNumber(v).intValue(); } } protected final long _parseLongPrimitive(JsonParser p, DeserializationContext ctxt) - throws IOException + throws IOException { - if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) { - return p.getLongValue(); - } - switch (p.getCurrentTokenId()) { + String text; + switch (p.currentTokenId()) { case JsonTokenId.ID_STRING: - String text = p.getText().trim(); - if (_isEmptyOrTextualNull(text)) { - _verifyNullForPrimitiveCoercion(ctxt, text); + text = p.getText(); + break; + case JsonTokenId.ID_NUMBER_FLOAT: + final CoercionAction act = _checkFloatToIntCoercion(p, ctxt, Long.TYPE); + if (act == CoercionAction.AsNull) { return 0L; } - return _parseLongPrimitive(ctxt, text); - case JsonTokenId.ID_NUMBER_FLOAT: - if (!ctxt.isEnabled(DeserializationFeature.ACCEPT_FLOAT_AS_INT)) { - _failDoubleToIntCoercion(p, ctxt, "long"); + if (act == CoercionAction.AsEmpty) { + return 0L; } return p.getValueAsLong(); + case JsonTokenId.ID_NUMBER_INT: + return p.getLongValue(); case JsonTokenId.ID_NULL: _verifyNullForPrimitive(ctxt); return 0L; + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, Long.TYPE); + break; case JsonTokenId.ID_START_ARRAY: if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { p.nextToken(); @@ -345,9 +794,25 @@ public abstract class StdDeserializer<T> _verifyEndArrayForSingle(p, ctxt); return parsed; } - break; + // fall through + default: + return ((Number) ctxt.handleUnexpectedToken(Long.TYPE, p)).longValue(); } - return ((Number) ctxt.handleUnexpectedToken(_valueClass, p)).longValue(); + + final CoercionAction act = _checkFromStringCoercion(ctxt, text, + LogicalType.Integer, Long.TYPE); + if (act == CoercionAction.AsNull) { + return 0L; // no need to check as does not come from `null`, explicit coercion + } + if (act == CoercionAction.AsEmpty) { + return 0L; + } + text = text.trim(); + if (_hasTextualNull(text)) { + _verifyNullForPrimitiveCoercion(ctxt, text); + return 0L; + } + return _parseLongPrimitive(ctxt, text); } /** @@ -359,8 +824,8 @@ public abstract class StdDeserializer<T> return NumberInput.parseLong(text); } catch (IllegalArgumentException iae) { } { - Number v = (Number) ctxt.handleWeirdStringValue(_valueClass, text, - "not a valid long value"); + Number v = (Number) ctxt.handleWeirdStringValue(Long.TYPE, text, + "not a valid `long` value"); return _nonNullNumber(v).longValue(); } } @@ -368,22 +833,21 @@ public abstract class StdDeserializer<T> protected final float _parseFloatPrimitive(JsonParser p, DeserializationContext ctxt) throws IOException { - if (p.hasToken(JsonToken.VALUE_NUMBER_FLOAT)) { - return p.getFloatValue(); - } - switch (p.getCurrentTokenId()) { + String text; + switch (p.currentTokenId()) { case JsonTokenId.ID_STRING: - String text = p.getText().trim(); - if (_isEmptyOrTextualNull(text)) { - _verifyNullForPrimitiveCoercion(ctxt, text); - return 0.0f; - } - return _parseFloatPrimitive(ctxt, text); + text = p.getText(); + break; case JsonTokenId.ID_NUMBER_INT: + case JsonTokenId.ID_NUMBER_FLOAT: return p.getFloatValue(); case JsonTokenId.ID_NULL: _verifyNullForPrimitive(ctxt); - return 0.0f; + return 0f; + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, Float.TYPE); + break; case JsonTokenId.ID_START_ARRAY: if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { p.nextToken(); @@ -391,11 +855,26 @@ public abstract class StdDeserializer<T> _verifyEndArrayForSingle(p, ctxt); return parsed; } - break; + // fall through + default: + return ((Number) ctxt.handleUnexpectedToken(Float.TYPE, p)).floatValue(); } - // Otherwise, no can do: - return ((Number) ctxt.handleUnexpectedToken(_valueClass, p)).floatValue(); - } + + final CoercionAction act = _checkFromStringCoercion(ctxt, text, + LogicalType.Integer, Float.TYPE); + if (act == CoercionAction.AsNull) { + return 0.0f; // no need to check as does not come from `null`, explicit coercion + } + if (act == CoercionAction.AsEmpty) { + return 0.0f; + } + text = text.trim(); + if (_hasTextualNull(text)) { + _verifyNullForPrimitiveCoercion(ctxt, text); + return 0.0f; + } + return _parseFloatPrimitive(ctxt, text); +} /** * @since 2.9 @@ -421,30 +900,29 @@ public abstract class StdDeserializer<T> try { return Float.parseFloat(text); } catch (IllegalArgumentException iae) { } - Number v = (Number) ctxt.handleWeirdStringValue(_valueClass, text, - "not a valid float value"); + Number v = (Number) ctxt.handleWeirdStringValue(Float.TYPE, text, + "not a valid `float` value"); return _nonNullNumber(v).floatValue(); } protected final double _parseDoublePrimitive(JsonParser p, DeserializationContext ctxt) throws IOException { - if (p.hasToken(JsonToken.VALUE_NUMBER_FLOAT)) { - return p.getDoubleValue(); - } - switch (p.getCurrentTokenId()) { + String text; + switch (p.currentTokenId()) { case JsonTokenId.ID_STRING: - String text = p.getText().trim(); - if (_isEmptyOrTextualNull(text)) { - _verifyNullForPrimitiveCoercion(ctxt, text); - return 0.0; - } - return _parseDoublePrimitive(ctxt, text); + text = p.getText(); + break; case JsonTokenId.ID_NUMBER_INT: + case JsonTokenId.ID_NUMBER_FLOAT: return p.getDoubleValue(); case JsonTokenId.ID_NULL: _verifyNullForPrimitive(ctxt); return 0.0; + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, Double.TYPE); + break; case JsonTokenId.ID_START_ARRAY: if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { p.nextToken(); @@ -452,10 +930,25 @@ public abstract class StdDeserializer<T> _verifyEndArrayForSingle(p, ctxt); return parsed; } - break; + // fall through + default: + return ((Number) ctxt.handleUnexpectedToken(Double.TYPE, p)).doubleValue(); + } + + final CoercionAction act = _checkFromStringCoercion(ctxt, text, + LogicalType.Integer, Double.TYPE); + if (act == CoercionAction.AsNull) { + return 0.0; // no need to check as does not come from `null`, explicit coercion + } + if (act == CoercionAction.AsEmpty) { + return 0.0; + } + text = text.trim(); + if (_hasTextualNull(text)) { + _verifyNullForPrimitiveCoercion(ctxt, text); + return 0.0; } - // Otherwise, no can do: - return ((Number) ctxt.handleUnexpectedToken(_valueClass, p)).doubleValue(); + return _parseDoublePrimitive(ctxt, text); } /** @@ -482,19 +975,34 @@ public abstract class StdDeserializer<T> break; } try { - return parseDouble(text); + return _parseDouble(text); } catch (IllegalArgumentException iae) { } - Number v = (Number) ctxt.handleWeirdStringValue(_valueClass, text, - "not a valid double value (as String to convert)"); + Number v = (Number) ctxt.handleWeirdStringValue(Double.TYPE, text, + "not a valid `double` value (as String to convert)"); return _nonNullNumber(v).doubleValue(); } + /** + * Helper method for encapsulating calls to low-level double value parsing; single place + * just because we need a work-around that must be applied to all calls. + */ + protected final static double _parseDouble(String numStr) throws NumberFormatException + { + // avoid some nasty float representations... but should it be MIN_NORMAL or MIN_VALUE? + if (NumberInput.NASTY_SMALL_DOUBLE.equals(numStr)) { + return Double.MIN_NORMAL; // since 2.7; was MIN_VALUE prior + } + return Double.parseDouble(numStr); + } + protected java.util.Date _parseDate(JsonParser p, DeserializationContext ctxt) throws IOException { - switch (p.getCurrentTokenId()) { + String text; + switch (p.currentTokenId()) { case JsonTokenId.ID_STRING: - return _parseDate(p.getText().trim(), ctxt); + text = p.getText(); + break; case JsonTokenId.ID_NUMBER_INT: { long ts; @@ -504,40 +1012,51 @@ public abstract class StdDeserializer<T> // (but leave both until 3.0) } catch (JsonParseException | InputCoercionException e) { Number v = (Number) ctxt.handleWeirdNumberValue(_valueClass, p.getNumberValue(), - "not a valid 64-bit long for creating `java.util.Date`"); + "not a valid 64-bit `long` for creating `java.util.Date`"); ts = v.longValue(); } return new java.util.Date(ts); } case JsonTokenId.ID_NULL: return (java.util.Date) getNullValue(ctxt); + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + case JsonTokenId.ID_START_OBJECT: + text = ctxt.extractScalarFromObject(p, this, _valueClass); + break; case JsonTokenId.ID_START_ARRAY: return _parseDateFromArray(p, ctxt); + default: + return (java.util.Date) ctxt.handleUnexpectedToken(_valueClass, p); } - return (java.util.Date) ctxt.handleUnexpectedToken(_valueClass, p); + + return _parseDate(text.trim(), ctxt); } // @since 2.9 protected java.util.Date _parseDateFromArray(JsonParser p, DeserializationContext ctxt) throws IOException { - JsonToken t; - if (ctxt.hasSomeOfFeatures(F_MASK_ACCEPT_ARRAYS)) { - t = p.nextToken(); + final CoercionAction act = _findCoercionFromEmptyArray(ctxt); + final boolean unwrap = ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS); + + if (unwrap || (act != CoercionAction.Fail)) { + JsonToken t = p.nextToken(); if (t == JsonToken.END_ARRAY) { - if (ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)) { + switch (act) { + case AsEmpty: + return (java.util.Date) getEmptyValue(ctxt); + case AsNull: + case TryConvert: return (java.util.Date) getNullValue(ctxt); + default: } - } - if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { + } else if (unwrap) { final Date parsed = _parseDate(p, ctxt); _verifyEndArrayForSingle(p, ctxt); return parsed; } - } else { - t = p.getCurrentToken(); } - return (java.util.Date) ctxt.handleUnexpectedToken(_valueClass, t, p, null); + return (java.util.Date) ctxt.handleUnexpectedToken(_valueClass, JsonToken.START_ARRAY, p, null); } /** @@ -548,8 +1067,20 @@ public abstract class StdDeserializer<T> { try { // Take empty Strings to mean 'empty' Value, usually 'null': - if (_isEmptyOrTextualNull(value)) { - return (java.util.Date) getNullValue(ctxt); + if (value.length() == 0) { + final CoercionAction act = _checkFromStringCoercion(ctxt, value); + switch (act) { // note: Fail handled above + case AsEmpty: + return new java.util.Date(0L); + case AsNull: + case TryConvert: + default: + } + return null; + } + // 10-Jun-2020, tatu: Legacy handling from pre-2.12... should we still have it? + if (_hasTextualNull(value)) { + return null; } return ctxt.parseDate(value); } catch (IllegalArgumentException iae) { @@ -560,19 +1091,6 @@ public abstract class StdDeserializer<T> } /** - * Helper method for encapsulating calls to low-level double value parsing; single place - * just because we need a work-around that must be applied to all calls. - */ - protected final static double parseDouble(String numStr) throws NumberFormatException - { - // avoid some nasty float representations... but should it be MIN_NORMAL or MIN_VALUE? - if (NumberInput.NASTY_SMALL_DOUBLE.equals(numStr)) { - return Double.MIN_NORMAL; // since 2.7; was MIN_VALUE prior - } - return Double.parseDouble(numStr); - } - - /** * Helper method used for accessing String value, if possible, doing * necessary conversion or throwing exception as necessary. * @@ -580,12 +1098,11 @@ public abstract class StdDeserializer<T> */ protected final String _parseString(JsonParser p, DeserializationContext ctxt) throws IOException { - JsonToken t = p.getCurrentToken(); - if (t == JsonToken.VALUE_STRING) { + if (p.hasToken(JsonToken.VALUE_STRING)) { return p.getText(); } // 07-Nov-2019, tatu: [databind#2535] Need to support byte[]->Base64 same as `StringDeserializer` - if (t == JsonToken.VALUE_EMBEDDED_OBJECT) { + if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { Object ob = p.getEmbeddedObject(); if (ob instanceof byte[]) { return ctxt.getBase64Variant().encode((byte[]) ob, false); @@ -596,18 +1113,11 @@ public abstract class StdDeserializer<T> // otherwise, try conversion using toString()... return ob.toString(); } - - // 07-Nov-2016, tatu: Caller should take care of unwrapping and there shouldn't - // be need for extra pass here... - /* - // [databind#381] - if ((t == JsonToken.START_ARRAY) && ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { - p.nextToken(); - final String parsed = _parseString(p, ctxt); - _verifyEndArrayForSingle(p, ctxt); - return parsed; + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + if (p.hasToken(JsonToken.START_OBJECT)) { + return ctxt.extractScalarFromObject(p, this, _valueClass); } - */ + String value = p.getValueAsString(); if (value != null) { return value; @@ -616,36 +1126,6 @@ public abstract class StdDeserializer<T> } /** - * Helper method that may be used to support fallback for Empty String / Empty Array - * non-standard representations; usually for things serialized as JSON Objects. - * - * @since 2.5 - */ - @SuppressWarnings("unchecked") - protected T _deserializeFromEmpty(JsonParser p, DeserializationContext ctxt) - throws IOException - { - JsonToken t = p.getCurrentToken(); - if (t == JsonToken.START_ARRAY) { - if (ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)) { - t = p.nextToken(); - if (t == JsonToken.END_ARRAY) { - return null; - } - return (T) ctxt.handleUnexpectedToken(handledType(), p); - } - } else if (t == JsonToken.VALUE_STRING) { - if (ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)) { - String str = p.getText().trim(); - if (str.isEmpty()) { - return null; - } - } - } - return (T) ctxt.handleUnexpectedToken(handledType(), p); - } - - /** * Helper method called to determine if we are seeing String value of * "null", and, further, that it should be coerced to null just like * null token. @@ -656,13 +1136,6 @@ public abstract class StdDeserializer<T> return "null".equals(value); } - /** - * @since 2.9 - */ - protected boolean _isEmptyOrTextualNull(String value) { - return value.isEmpty() || "null".equals(value); - } - protected final boolean _isNegInf(String text) { return "-Infinity".equals(text) || "-INF".equals(text); } @@ -673,92 +1146,140 @@ public abstract class StdDeserializer<T> protected final boolean _isNaN(String text) { return "NaN".equals(text); } + // @since 2.12 + protected final static boolean _isBlank(String text) + { + final int len = text.length(); + for (int i = 0; i < len; ++i) { + if (text.charAt(i) > 0x0020) { + return false; + } + } + return true; + } + /* - /********************************************************** - /* Helper methods for sub-classes regarding decoding from - /* alternate representations - /********************************************************** + /**************************************************** + /* Helper methods for sub-classes, new (2.12+) + /**************************************************** */ /** - * Helper method that allows easy support for array-related {@link DeserializationFeature}s - * `ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT` and `UNWRAP_SINGLE_VALUE_ARRAYS`: checks for either - * empty array, or single-value array-wrapped value (respectively), and either reports - * an exception (if no match, or feature(s) not enabled), or returns appropriate - * result value. - *<p> - * This method should NOT be called if Array representation is explicitly supported - * for type: it should only be called in case it is otherwise unrecognized. - *<p> - * NOTE: in case of unwrapped single element, will handle actual decoding - * by calling {@link #_deserializeWrappedValue}, which by default calls - * {@link #deserialize(JsonParser, DeserializationContext)}. - * - * @since 2.9 + * @since 2.12 */ - protected T _deserializeFromArray(JsonParser p, DeserializationContext ctxt) throws IOException + protected CoercionAction _checkFromStringCoercion(DeserializationContext ctxt, String value) + throws IOException { - JsonToken t; - if (ctxt.hasSomeOfFeatures(F_MASK_ACCEPT_ARRAYS)) { - t = p.nextToken(); - if (t == JsonToken.END_ARRAY) { - if (ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)) { - return getNullValue(ctxt); - } - } - if (ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) { - final T parsed = deserialize(p, ctxt); - if (p.nextToken() != JsonToken.END_ARRAY) { - handleMissingEndArrayForSingle(p, ctxt); - } - return parsed; - } + return _checkFromStringCoercion(ctxt, value, logicalType(), handledType()); + } + + /** + * @since 2.12 + */ + protected CoercionAction _checkFromStringCoercion(DeserializationContext ctxt, String value, + LogicalType logicalType, Class<?> rawTargetType) + throws IOException + { + final CoercionAction act; + + if (value.length() == 0) { + act = ctxt.findCoercionAction(logicalType, rawTargetType, + CoercionInputShape.EmptyString); + return _checkCoercionActionFail(ctxt, act, "empty String (\"\")"); + } else if (_isBlank(value)) { + act = ctxt.findCoercionFromBlankString(logicalType, rawTargetType, CoercionAction.Fail); + return _checkCoercionActionFail(ctxt, act, "blank String (all whitespace)"); } else { - t = p.getCurrentToken(); + act = ctxt.findCoercionAction(logicalType, rawTargetType, CoercionInputShape.String); + if (act == CoercionAction.Fail) { + // since it MIGHT (but might not), create desc here, do not use helper + ctxt.reportInputMismatch(this, +"Cannot coerce String value (\"%s\") to %s (but might if coercion using `CoercionConfig` was enabled)", +value, _coercedTypeDesc()); + } } - @SuppressWarnings("unchecked") - T result = (T) ctxt.handleUnexpectedToken(getValueType(ctxt), p.getCurrentToken(), p, null); - return result; + return act; } /** - * Helper called to support {@link DeserializationFeature#UNWRAP_SINGLE_VALUE_ARRAYS}: - * default implementation simply calls - * {@link #deserialize(JsonParser, DeserializationContext)}, - * but handling may be overridden. - * - * @since 2.9 + * @since 2.12 */ - protected T _deserializeWrappedValue(JsonParser p, DeserializationContext ctxt) throws IOException + protected CoercionAction _checkFloatToIntCoercion(JsonParser p, DeserializationContext ctxt, + Class<?> rawTargetType) + throws IOException { - // 23-Mar-2017, tatu: Let's specifically block recursive resolution to avoid - // either supporting nested arrays, or to cause infinite looping. - if (p.hasToken(JsonToken.START_ARRAY)) { - String msg = String.format( -"Cannot deserialize instance of %s out of %s token: nested Arrays not allowed with %s", - ClassUtil.nameOf(_valueClass), JsonToken.START_ARRAY, - "DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS"); - @SuppressWarnings("unchecked") - T result = (T) ctxt.handleUnexpectedToken(getValueType(ctxt), p.getCurrentToken(), p, msg); - return result; + final CoercionAction act = ctxt.findCoercionAction(LogicalType.Integer, + rawTargetType, CoercionInputShape.Float); + if (act == CoercionAction.Fail) { + _checkCoercionActionFail(ctxt, act, "Floating-point value ("+p.getText()+")"); } - return (T) deserialize(p, ctxt); + return act; } - /* - /**************************************************** - /* Helper methods for sub-classes, coercions - /**************************************************** + /** + * @since 2.12 */ + protected Boolean _coerceBooleanFromInt(JsonParser p, DeserializationContext ctxt, + Class<?> rawTargetType) + throws IOException + { + CoercionAction act = ctxt.findCoercionAction(LogicalType.Boolean, rawTargetType, CoercionInputShape.Integer); + switch (act) { + case Fail: + _checkCoercionActionFail(ctxt, act, "Integer value ("+p.getText()+")"); + break; + case AsNull: + return null; + case AsEmpty: + return Boolean.FALSE; + default: + } + // 13-Oct-2016, tatu: As per [databind#1324], need to be careful wrt + // degenerate case of huge integers, legal in JSON. + // Also note that number tokens can not have WS to trim: + if (p.getNumberType() == NumberType.INT) { + // but minor optimization for common case is possible: + return p.getIntValue() != 0; + } + return !"0".equals(p.getText()); + } - protected void _failDoubleToIntCoercion(JsonParser p, DeserializationContext ctxt, - String type) throws IOException + protected CoercionAction _checkCoercionActionFail(DeserializationContext ctxt, + CoercionAction act, String inputDesc) throws IOException { - ctxt.reportInputMismatch(handledType(), -"Cannot coerce a floating-point value ('%s') into %s (enable `DeserializationFeature.ACCEPT_FLOAT_AS_INT` to allow)", - p.getValueAsString(), type); + if (act == CoercionAction.Fail) { + ctxt.reportInputMismatch(this, +"Cannot coerce %s to %s (but could if coercion was enabled using `CoercionConfig`)", +inputDesc, _coercedTypeDesc()); + } + return act; + } + + /** + * Method called when otherwise unrecognized String value is encountered for + * a non-primitive type: should see if it is String value {@code "null"}, and if so, + * whether it is acceptable according to configuration or not + * + * @since 2.12 + */ + protected boolean _checkTextualNull(DeserializationContext ctxt, String text) + throws JsonMappingException + { + if (_hasTextualNull(text)) { + if (!ctxt.isEnabled(MapperFeature.ALLOW_COERCION_OF_SCALARS)) { + _reportFailedNullCoerce(ctxt, true, MapperFeature.ALLOW_COERCION_OF_SCALARS, "String \"null\""); + } + return true; + } + return false; } + /* + /********************************************************************** + /* Helper methods for sub-classes, coercions, older (pre-2.12), non-deprecated + /********************************************************************** + */ + /** * Helper method called in case where an integral number is encountered, but * config settings suggest that a coercion may be needed to "upgrade" @@ -779,29 +1300,35 @@ public abstract class StdDeserializer<T> if (DeserializationFeature.USE_LONG_FOR_INTS.enabledIn(feats)) { return p.getLongValue(); } - return p.getBigIntegerValue(); // should be optimal, whatever it is + return p.getNumberValue(); // should be optimal, whatever it is } /** - * Method to call when JSON `null` token is encountered. Note: only called when - * this deserializer encounters it but NOT when reached via property + * Method called to verify that {@code null} token from input is acceptable + * for primitive (unboxed) target type. It should NOT be called if {@code null} + * was received by other means (coerced due to configuration, or even from + * optionally acceptable String {@code "null"} token). * * @since 2.9 */ - protected Object _coerceNullToken(DeserializationContext ctxt, boolean isPrimitive) throws JsonMappingException + protected final void _verifyNullForPrimitive(DeserializationContext ctxt) throws JsonMappingException { - if (isPrimitive) { - _verifyNullForPrimitive(ctxt); + if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { + ctxt.reportInputMismatch(this, +"Cannot coerce `null` to %s (disable `DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES` to allow)", + _coercedTypeDesc()); } - return getNullValue(ctxt); } /** - * Method called when JSON String with value "null" is encountered. + * Method called to verify that text value {@code "null"} from input is acceptable + * for primitive (unboxed) target type. It should not be called if actual + * {@code null} token was received, or if null is a result of coercion from + * Some other input type. * * @since 2.9 */ - protected Object _coerceTextualNull(DeserializationContext ctxt, boolean isPrimitive) throws JsonMappingException + protected final void _verifyNullForPrimitiveCoercion(DeserializationContext ctxt, String str) throws JsonMappingException { Enum<?> feat; boolean enable; @@ -809,52 +1336,93 @@ public abstract class StdDeserializer<T> if (!ctxt.isEnabled(MapperFeature.ALLOW_COERCION_OF_SCALARS)) { feat = MapperFeature.ALLOW_COERCION_OF_SCALARS; enable = true; - } else if (isPrimitive && ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { + } else if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { feat = DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES; enable = false; } else { - return getNullValue(ctxt); + return; } - _reportFailedNullCoerce(ctxt, enable, feat, "String \"null\""); - return null; + String strDesc = str.isEmpty() ? "empty String (\"\")" : String.format("String \"%s\"", str); + _reportFailedNullCoerce(ctxt, enable, feat, strDesc); + } + + protected void _reportFailedNullCoerce(DeserializationContext ctxt, boolean state, Enum<?> feature, + String inputDesc) throws JsonMappingException + { + String enableDesc = state ? "enable" : "disable"; + ctxt.reportInputMismatch(this, "Cannot coerce %s to Null value as %s (%s `%s.%s` to allow)", + inputDesc, _coercedTypeDesc(), enableDesc, feature.getClass().getSimpleName(), feature.name()); } /** - * Method called when JSON String with value "" (that is, zero length) is encountered. + * Helper method called to get a description of type into which a scalar value coercion + * is (most likely) being applied, to be used for constructing exception messages + * on coerce failure. + * + * @return Message with backtick-enclosed name of type this deserializer supports * * @since 2.9 */ - protected Object _coerceEmptyString(DeserializationContext ctxt, boolean isPrimitive) throws JsonMappingException - { - Enum<?> feat; - boolean enable; + protected String _coercedTypeDesc() { + boolean structured; + String typeDesc; - if (!ctxt.isEnabled(MapperFeature.ALLOW_COERCION_OF_SCALARS)) { - feat = MapperFeature.ALLOW_COERCION_OF_SCALARS; - enable = true; - } else if (isPrimitive && ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { - feat = DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES; - enable = false; + JavaType t = getValueType(); + if ((t != null) && !t.isPrimitive()) { + structured = (t.isContainerType() || t.isReferenceType()); + typeDesc = ClassUtil.getTypeDescription(t); } else { - return getNullValue(ctxt); + Class<?> cls = handledType(); + structured = cls.isArray() || Collection.class.isAssignableFrom(cls) + || Map.class.isAssignableFrom(cls); + typeDesc = ClassUtil.getClassDescription(cls); } - _reportFailedNullCoerce(ctxt, enable, feat, "empty String (\"\")"); - return null; + if (structured) { + return "element of "+typeDesc; + } + return typeDesc+" value"; } - // @since 2.9 - protected final void _verifyNullForPrimitive(DeserializationContext ctxt) throws JsonMappingException + /* + /********************************************************************** + /* Helper methods for sub-classes, coercions, older (pre-2.12), deprecated + /********************************************************************** + */ + + @Deprecated // since 2.12 + protected boolean _parseBooleanFromInt(JsonParser p, DeserializationContext ctxt) + throws IOException { - if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { - ctxt.reportInputMismatch(this, -"Cannot coerce `null` %s (disable `DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES` to allow)", - _coercedTypeDesc()); + // 13-Oct-2016, tatu: As per [databind#1324], need to be careful wrt + // degenerate case of huge integers, legal in JSON. + // ... this is, on the other hand, probably wrong/sub-optimal for non-JSON + // input. For now, no rea + _verifyNumberForScalarCoercion(ctxt, p); + // Anyway, note that since we know it's valid (JSON) integer, it can't have + // extra whitespace to trim. + return !"0".equals(p.getText()); + } + + /** + * @deprecated Since 2.12 use {@link #_checkFromStringCoercion} instead + */ + @Deprecated + protected void _verifyStringForScalarCoercion(DeserializationContext ctxt, String str) throws JsonMappingException + { + MapperFeature feat = MapperFeature.ALLOW_COERCION_OF_SCALARS; + if (!ctxt.isEnabled(feat)) { + ctxt.reportInputMismatch(this, "Cannot coerce String \"%s\" to %s (enable `%s.%s` to allow)", + str, _coercedTypeDesc(), feat.getClass().getSimpleName(), feat.name()); } } - // NOTE: only for primitive Scalars - // @since 2.9 - protected final void _verifyNullForPrimitiveCoercion(DeserializationContext ctxt, String str) throws JsonMappingException + /** + * Method called when JSON String with value "" (that is, zero length) is encountered. + * + * @deprecated Since 2.12 + */ + @Deprecated + protected Object _coerceEmptyString(DeserializationContext ctxt, boolean isPrimitive) throws JsonMappingException { Enum<?> feat; boolean enable; @@ -862,18 +1430,26 @@ public abstract class StdDeserializer<T> if (!ctxt.isEnabled(MapperFeature.ALLOW_COERCION_OF_SCALARS)) { feat = MapperFeature.ALLOW_COERCION_OF_SCALARS; enable = true; - } else if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { + } else if (isPrimitive && ctxt.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)) { feat = DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES; enable = false; } else { - return; + return getNullValue(ctxt); } - String strDesc = str.isEmpty() ? "empty String (\"\")" : String.format("String \"%s\"", str); - _reportFailedNullCoerce(ctxt, enable, feat, strDesc); + _reportFailedNullCoerce(ctxt, enable, feat, "empty String (\"\")"); + return null; } - // NOTE: for non-primitive Scalars - // @since 2.9 + @Deprecated // since 2.12 + protected void _failDoubleToIntCoercion(JsonParser p, DeserializationContext ctxt, + String type) throws IOException + { + ctxt.reportInputMismatch(handledType(), +"Cannot coerce a floating-point value ('%s') into %s (enable `DeserializationFeature.ACCEPT_FLOAT_AS_INT` to allow)", + p.getValueAsString(), type); + } + + @Deprecated // since 2.12 protected final void _verifyNullForScalarCoercion(DeserializationContext ctxt, String str) throws JsonMappingException { if (!ctxt.isEnabled(MapperFeature.ALLOW_COERCION_OF_SCALARS)) { @@ -882,17 +1458,7 @@ public abstract class StdDeserializer<T> } } - // @since 2.9 - protected void _verifyStringForScalarCoercion(DeserializationContext ctxt, String str) throws JsonMappingException - { - MapperFeature feat = MapperFeature.ALLOW_COERCION_OF_SCALARS; - if (!ctxt.isEnabled(feat)) { - ctxt.reportInputMismatch(this, "Cannot coerce String \"%s\" %s (enable `%s.%s` to allow)", - str, _coercedTypeDesc(), feat.getClass().getSimpleName(), feat.name()); - } - } - - // @since 2.9 + @Deprecated // since 2.12 protected void _verifyNumberForScalarCoercion(DeserializationContext ctxt, JsonParser p) throws IOException { MapperFeature feat = MapperFeature.ALLOW_COERCION_OF_SCALARS; @@ -900,47 +1466,31 @@ public abstract class StdDeserializer<T> // 31-Mar-2017, tatu: Since we don't know (or this deep, care) about exact type, // access as a String: may require re-encoding by parser which should be fine String valueDesc = p.getText(); - ctxt.reportInputMismatch(this, "Cannot coerce Number (%s) %s (enable `%s.%s` to allow)", + ctxt.reportInputMismatch(this, "Cannot coerce Number (%s) to %s (enable `%s.%s` to allow)", valueDesc, _coercedTypeDesc(), feat.getClass().getSimpleName(), feat.name()); } } - protected void _reportFailedNullCoerce(DeserializationContext ctxt, boolean state, Enum<?> feature, - String inputDesc) throws JsonMappingException + @Deprecated // since 2.12 + protected Object _coerceNullToken(DeserializationContext ctxt, boolean isPrimitive) throws JsonMappingException { - String enableDesc = state ? "enable" : "disable"; - ctxt.reportInputMismatch(this, "Cannot coerce %s to Null value %s (%s `%s.%s` to allow)", - inputDesc, _coercedTypeDesc(), enableDesc, feature.getClass().getSimpleName(), feature.name()); + if (isPrimitive) { + _verifyNullForPrimitive(ctxt); + } + return getNullValue(ctxt); } - /** - * Helper method called to get a description of type into which a scalar value coercion - * is (most likely) being applied, to be used for constructing exception messages - * on coerce failure. - * - * @return Message with backtick-enclosed name of type this deserializer supports - * - * @since 2.9 - */ - protected String _coercedTypeDesc() { - boolean structured; - String typeDesc; - - JavaType t = getValueType(); - if ((t != null) && !t.isPrimitive()) { - structured = (t.isContainerType() || t.isReferenceType()); - // 21-Jul-2017, tatu: Probably want to change this (JavaType.toString() not very good) but... - typeDesc = "'"+t.toString()+"'"; - } else { - Class<?> cls = handledType(); - structured = cls.isArray() || Collection.class.isAssignableFrom(cls) - || Map.class.isAssignableFrom(cls); - typeDesc = ClassUtil.nameOf(cls); - } - if (structured) { - return "as content of type "+typeDesc; + @Deprecated // since 2.12 + protected Object _coerceTextualNull(DeserializationContext ctxt, boolean isPrimitive) throws JsonMappingException { + if (!ctxt.isEnabled(MapperFeature.ALLOW_COERCION_OF_SCALARS)) { + _reportFailedNullCoerce(ctxt, true, MapperFeature.ALLOW_COERCION_OF_SCALARS, "String \"null\""); } - return "for type "+typeDesc; + return getNullValue(ctxt); + } + + @Deprecated // since 2.12 + protected boolean _isEmptyOrTextualNull(String value) { + return value.isEmpty() || "null".equals(value); } /* @@ -968,6 +1518,9 @@ public abstract class StdDeserializer<T> /** * Helper method to check whether given text refers to what looks like a clean simple * integer number, consisting of optional sign followed by a sequence of digits. + *<p> + * Note that definition is quite loose as leading zeroes are allowed, in addition + * to plus sign (not just minus). */ protected final boolean _isIntNumber(String text) { @@ -975,7 +1528,17 @@ public abstract class StdDeserializer<T> if (len > 0) { char c = text.charAt(0); // skip leading sign (plus not allowed for strict JSON numbers but...) - int i = (c == '-' || c == '+') ? 1 : 0; + int i; + + if (c == '-' || c == '+') { + if (len == 1) { + return false; + } + i = 1; + } else { + i = 0; + } + // We will allow leading for (; i < len; ++i) { int ch = text.charAt(i); if (ch > '9' || ch < '0') { @@ -1181,6 +1744,24 @@ public abstract class StdDeserializer<T> return null; } + // @since 2.12 + protected CoercionAction _findCoercionFromEmptyString(DeserializationContext ctxt) { + return ctxt.findCoercionAction(logicalType(), handledType(), + CoercionInputShape.EmptyString); + } + + // @since 2.12 + protected CoercionAction _findCoercionFromEmptyArray(DeserializationContext ctxt) { + return ctxt.findCoercionAction(logicalType(), handledType(), + CoercionInputShape.EmptyArray); + } + + // @since 2.12 + protected CoercionAction _findCoercionFromBlankString(DeserializationContext ctxt) { + return ctxt.findCoercionFromBlankString(logicalType(), handledType(), + CoercionAction.Fail); + } + /* /********************************************************** /* Helper methods for sub-classes, problem reporting diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdScalarDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdScalarDeserializer.java index 62f27abef..7c21f672d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdScalarDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdScalarDeserializer.java @@ -5,6 +5,7 @@ import java.io.IOException; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.AccessPattern; /** @@ -20,22 +21,16 @@ public abstract class StdScalarDeserializer<T> extends StdDeserializer<T> // since 2.5 protected StdScalarDeserializer(StdScalarDeserializer<?> src) { super(src); } - - @Override - public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException { - return typeDeserializer.deserializeTypedFromScalar(p, ctxt); - } - /** - * Overridden to simply call <code>deserialize()</code> method that does not take value - * to update, since scalar values are usually non-mergeable. + /* + /********************************************************************** + /* Overridden accessors + /********************************************************************** */ - @Override // since 2.9 - public T deserialize(JsonParser p, DeserializationContext ctxt, T intoValue) throws IOException { - // 25-Oct-2016, tatu: And if attempt is made, see if we are to complain... - ctxt.handleBadMerge(this); - // if that does not report an exception we can just delegate - return deserialize(p, ctxt); + + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.OtherScalar; } /** @@ -59,4 +54,27 @@ public abstract class StdScalarDeserializer<T> extends StdDeserializer<T> public AccessPattern getEmptyAccessPattern() { return AccessPattern.CONSTANT; } + + /* + /********************************************************************** + /* Default deserialization method impls + /********************************************************************** + */ + + @Override + public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException { + return typeDeserializer.deserializeTypedFromScalar(p, ctxt); + } + + /** + * Overridden to simply call <code>deserialize()</code> method that does not take value + * to update, since scalar values are usually non-mergeable. + */ + @Override // since 2.9 + public T deserialize(JsonParser p, DeserializationContext ctxt, T intoValue) throws IOException { + // 25-Oct-2016, tatu: And if attempt is made, see if we are to complain... + ctxt.handleBadMerge(this); + // if that does not report an exception we can just delegate + return deserialize(p, ctxt); + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java index e4ac0fd6f..2f4151271 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java @@ -6,7 +6,6 @@ import java.lang.reflect.InvocationTargetException; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.deser.*; -import com.fasterxml.jackson.databind.introspect.AnnotatedParameter; import com.fasterxml.jackson.databind.introspect.AnnotatedWithParams; import com.fasterxml.jackson.databind.util.ClassUtil; @@ -66,9 +65,6 @@ public class StdValueInstantiator protected AnnotatedWithParams _fromDoubleCreator; protected AnnotatedWithParams _fromBooleanCreator; - // // // Incomplete creator - protected AnnotatedParameter _incompleteParameter; - /* /********************************************************** /* Life-cycle @@ -165,10 +161,6 @@ public class StdValueInstantiator _fromBooleanCreator = creator; } - public void configureIncompleteParameter(AnnotatedParameter parameter) { - _incompleteParameter = parameter; - } - /* /********************************************************** /* Public API implementation; metadata @@ -259,7 +251,7 @@ public class StdValueInstantiator /* Public API implementation; instantiation from JSON Object /********************************************************** */ - + @Override public Object createUsingDefault(DeserializationContext ctxt) throws IOException { @@ -319,17 +311,17 @@ public class StdValueInstantiator @Override public Object createFromString(DeserializationContext ctxt, String value) throws IOException { - if (_fromStringCreator == null) { - return _createFromStringFallbacks(ctxt, value); - } - try { - return _fromStringCreator.call1(value); - } catch (Throwable t) { - return ctxt.handleInstantiationProblem(_fromStringCreator.getDeclaringClass(), - value, rewrapCtorProblem(ctxt, t)); + if (_fromStringCreator != null) { + try { + return _fromStringCreator.call1(value); + } catch (Throwable t) { + return ctxt.handleInstantiationProblem(_fromStringCreator.getDeclaringClass(), + value, rewrapCtorProblem(ctxt, t)); + } } + return super.createFromString(ctxt, value); } - + @Override public Object createFromInt(DeserializationContext ctxt, int value) throws IOException { @@ -400,7 +392,7 @@ public class StdValueInstantiator arg, rewrapCtorProblem(ctxt, t0)); } } - + /* /********************************************************** /* Extended API: configuration mutators, accessors @@ -427,11 +419,6 @@ public class StdValueInstantiator return _withArgsCreator; } - @Override - public AnnotatedParameter getIncompleteParameter() { - return _incompleteParameter; - } - /* /********************************************************** /* Internal methods diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StringArrayDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StringArrayDeserializer.java index a348a4019..798a92624 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StringArrayDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StringArrayDeserializer.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.deser.NullValueProvider; import com.fasterxml.jackson.databind.deser.impl.NullsConstantProvider; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.AccessPattern; import com.fasterxml.jackson.databind.util.ObjectBuffer; @@ -75,6 +76,11 @@ public final class StringArrayDeserializer _skipNullValues = NullsConstantProvider.isSkipper(nuller); } + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Array; + } + @Override // since 2.9 public Boolean supportsUpdate(DeserializationConfig config) { return Boolean.TRUE; @@ -144,7 +150,7 @@ public final class StringArrayDeserializer while (true) { String value = p.nextTextValue(); if (value == null) { - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == JsonToken.END_ARRAY) { break; } @@ -200,7 +206,7 @@ public final class StringArrayDeserializer */ String value; if (p.nextTextValue() == null) { - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == JsonToken.END_ARRAY) { break; } @@ -264,7 +270,7 @@ public final class StringArrayDeserializer while (true) { String value = p.nextTextValue(); if (value == null) { - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == JsonToken.END_ARRAY) { break; } @@ -304,12 +310,8 @@ public final class StringArrayDeserializer : _parseString(p, ctxt); return new String[] { value }; } - if (p.hasToken(JsonToken.VALUE_STRING) - && ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)) { - String str = p.getText(); - if (str.length() == 0) { - return null; - } + if (p.hasToken(JsonToken.VALUE_STRING)) { + return _deserializeFromString(p, ctxt); } return (String[]) ctxt.handleUnexpectedToken(_valueClass, p); } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StringCollectionDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StringCollectionDeserializer.java index 101d03b0f..f4ac7ea73 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StringCollectionDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StringCollectionDeserializer.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.deser.NullValueProvider; import com.fasterxml.jackson.databind.deser.ValueInstantiator; import com.fasterxml.jackson.databind.introspect.AnnotatedWithParams; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; /** * Specifically optimized version for {@link java.util.Collection}s @@ -90,7 +91,12 @@ public final class StringCollectionDeserializer // are involved return (_valueDeserializer == null) && (_delegateDeserializer == null); } - + + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Collection; + } + /* /********************************************************** /* Validation, post-processing @@ -194,7 +200,7 @@ public final class StringCollectionDeserializer result.add(value); continue; } - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == JsonToken.END_ARRAY) { break; } @@ -226,7 +232,7 @@ public final class StringCollectionDeserializer */ String value; if (p.nextTextValue() == null) { - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == JsonToken.END_ARRAY) { break; } @@ -271,11 +277,14 @@ public final class StringCollectionDeserializer ((_unwrapSingle == null) && ctxt.isEnabled(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)); if (!canWrap) { - return (Collection<String>) ctxt.handleUnexpectedToken(_containerType.getRawClass(), p); + if (p.hasToken(JsonToken.VALUE_STRING)) { + return _deserializeFromString(p, ctxt); + } + return (Collection<String>) ctxt.handleUnexpectedToken(_containerType, p); } // Strings are one of "native" (intrinsic) types, so there's never type deserializer involved JsonDeserializer<String> valueDes = _valueDeserializer; - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); String value; diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StringDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StringDeserializer.java index a4b25c060..2cdf67a1d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StringDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StringDeserializer.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; @JacksonStdImpl public class StringDeserializer extends StdScalarDeserializer<String> // non-final since 2.9 @@ -19,6 +20,11 @@ public class StringDeserializer extends StdScalarDeserializer<String> // non-fin public StringDeserializer() { super(String.class); } + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Textual; + } + // since 2.6, slightly faster lookups for this very common type @Override public boolean isCachable() { return true; } @@ -51,6 +57,10 @@ public class StringDeserializer extends StdScalarDeserializer<String> // non-fin // otherwise, try conversion using toString()... return ob.toString(); } + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + if (t == JsonToken.START_OBJECT) { + return ctxt.extractScalarFromObject(p, this, _valueClass); + } // allow coercions for other scalar types // 17-Jan-2018, tatu: Related to [databind#1853] avoid FIELD_NAME by ensuring it's // "real" scalar diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/ThrowableDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/ThrowableDeserializer.java index 087895682..65d5aa018 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/ThrowableDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/ThrowableDeserializer.java @@ -85,7 +85,7 @@ public class ThrowableDeserializer int pendingIx = 0; for (; !p.hasToken(JsonToken.END_OBJECT); p.nextToken()) { - String propName = p.getCurrentName(); + String propName = p.currentName(); SettableBeanProperty prop = _beanProperties.find(propName); p.nextToken(); // to point to field value diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/TokenBufferDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/TokenBufferDeserializer.java index 5435e97f4..cf2214960 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/TokenBufferDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/TokenBufferDeserializer.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.TokenBuffer; /** @@ -26,6 +27,11 @@ public class TokenBufferDeserializer extends StdScalarDeserializer<TokenBuffer> public TokenBufferDeserializer() { super(TokenBuffer.class); } + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Untyped; + } + @Override public TokenBuffer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { return createBufferInstance(p).deserialize(p, ctxt); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/UUIDDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/UUIDDeserializer.java index 797569e6e..65e8a87fe 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/UUIDDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/UUIDDeserializer.java @@ -26,6 +26,11 @@ public class UUIDDeserializer extends FromStringDeserializer<UUID> public UUIDDeserializer() { super(UUID.class); } + @Override // since 2.12 + public Object getEmptyValue(DeserializationContext ctxt) { + return new UUID(0L, 0L); + } + @Override protected UUID _deserialize(String id, DeserializationContext ctxt) throws IOException { @@ -62,15 +67,14 @@ public class UUIDDeserializer extends FromStringDeserializer<UUID> return new UUID(hi, lo); } - + @Override protected UUID _deserializeEmbedded(Object ob, DeserializationContext ctxt) throws IOException { if (ob instanceof byte[]) { return _fromBytes((byte[]) ob, ctxt); } - super._deserializeEmbedded(ob, ctxt); - return null; // never gets here + return super._deserializeEmbedded(ob, ctxt); } private UUID _badFormat(String uuidStr, DeserializationContext ctxt) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java index 2f0df1d78..ec949d469 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/UntypedObjectDeserializer.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.jackson.databind.util.ObjectBuffer; @@ -197,9 +198,11 @@ public class UntypedObjectDeserializer && getClass() == UntypedObjectDeserializer.class) { return Vanilla.instance(preventMerge); } + if (preventMerge != _nonMerging) { return new UntypedObjectDeserializer(this, preventMerge); } + return this; } @@ -222,6 +225,11 @@ public class UntypedObjectDeserializer return true; } + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Untyped; + } + @Override // since 2.9 public Boolean supportsUpdate(DeserializationConfig config) { // 21-Apr-2017, tatu: Bit tricky... some values, yes. So let's say "dunno" @@ -231,7 +239,7 @@ public class UntypedObjectDeserializer @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - switch (p.getCurrentTokenId()) { + switch (p.currentTokenId()) { case JsonTokenId.ID_START_OBJECT: case JsonTokenId.ID_FIELD_NAME: // 28-Oct-2015, tatu: [databind#989] We may also be given END_OBJECT (similar to FIELD_NAME), @@ -298,7 +306,7 @@ public class UntypedObjectDeserializer public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException { - switch (p.getCurrentTokenId()) { + switch (p.currentTokenId()) { // First: does it look like we had type id wrapping of some kind? case JsonTokenId.ID_START_ARRAY: case JsonTokenId.ID_START_OBJECT: @@ -357,7 +365,7 @@ public class UntypedObjectDeserializer return deserialize(p, ctxt); } - switch (p.getCurrentTokenId()) { + switch (p.currentTokenId()) { case JsonTokenId.ID_START_OBJECT: case JsonTokenId.ID_FIELD_NAME: // We may also be given END_OBJECT (similar to FIELD_NAME), @@ -487,7 +495,7 @@ public class UntypedObjectDeserializer { String key1; - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == JsonToken.START_OBJECT) { key1 = p.nextFieldName(); @@ -501,18 +509,17 @@ public class UntypedObjectDeserializer } if (key1 == null) { // empty map might work; but caller may want to modify... so better just give small modifiable - return new LinkedHashMap<String,Object>(2); + return new LinkedHashMap<>(2); } // minor optimization; let's handle 1 and 2 entry cases separately // 24-Mar-2015, tatu: Ideally, could use one of 'nextXxx()' methods, but for // that we'd need new method(s) in JsonDeserializer. So not quite yet. p.nextToken(); Object value1 = deserialize(p, ctxt); - String key2 = p.nextFieldName(); if (key2 == null) { // has to be END_OBJECT, then // single entry; but we want modifiable - LinkedHashMap<String, Object> result = new LinkedHashMap<String, Object>(2); + LinkedHashMap<String, Object> result = new LinkedHashMap<>(2); result.put(key1, value1); return result; } @@ -520,25 +527,74 @@ public class UntypedObjectDeserializer Object value2 = deserialize(p, ctxt); String key = p.nextFieldName(); - if (key == null) { - LinkedHashMap<String, Object> result = new LinkedHashMap<String, Object>(4); + LinkedHashMap<String, Object> result = new LinkedHashMap<>(4); result.put(key1, value1); - result.put(key2, value2); + if (result.put(key2, value2) != null) { + // 22-May-2020, tatu: [databind#2733] may need extra handling + return _mapObjectWithDups(p, ctxt, result, key1, value1, value2, key); + } return result; } // And then the general case; default map size is 16 - LinkedHashMap<String, Object> result = new LinkedHashMap<String, Object>(); + LinkedHashMap<String, Object> result = new LinkedHashMap<>(); result.put(key1, value1); - result.put(key2, value2); + if (result.put(key2, value2) != null) { + // 22-May-2020, tatu: [databind#2733] may need extra handling + return _mapObjectWithDups(p, ctxt, result, key1, value1, value2, key); + } do { p.nextToken(); - result.put(key, deserialize(p, ctxt)); + final Object newValue = deserialize(p, ctxt); + final Object oldValue = result.put(key, newValue); + if (oldValue != null) { + return _mapObjectWithDups(p, ctxt, result, key, oldValue, newValue, + p.nextFieldName()); + } } while ((key = p.nextFieldName()) != null); return result; } + // @since 2.12 (wrt [databind#2733] + protected Object _mapObjectWithDups(JsonParser p, DeserializationContext ctxt, + final Map<String, Object> result, String key, + Object oldValue, Object newValue, String nextKey) throws IOException + { + final boolean squashDups = ctxt.isEnabled(StreamReadCapability.DUPLICATE_PROPERTIES); + + if (squashDups) { + _squashDups(result, key, oldValue, newValue); + } + + while (nextKey != null) { + p.nextToken(); + newValue = deserialize(p, ctxt); + oldValue = result.put(nextKey, newValue); + if ((oldValue != null) && squashDups) { + _squashDups(result, key, oldValue, newValue); + } + nextKey = p.nextFieldName(); + } + + return result; + } + + @SuppressWarnings("unchecked") + private void _squashDups(final Map<String, Object> result, String key, + Object oldValue, Object newValue) + { + if (oldValue instanceof List<?>) { + ((List<Object>) oldValue).add(newValue); + result.put(key, oldValue); + } else { + ArrayList<Object> l = new ArrayList<>(); + l.add(oldValue); + l.add(newValue); + result.put(key, l); + } + } + /** * Method called to map a JSON Array into a Java Object array (Object[]). */ @@ -565,7 +621,7 @@ public class UntypedObjectDeserializer protected Object mapObject(JsonParser p, DeserializationContext ctxt, Map<Object,Object> m) throws IOException { - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == JsonToken.START_OBJECT) { t = p.nextToken(); } @@ -629,7 +685,12 @@ public class UntypedObjectDeserializer } return std; } - + + @Override // since 2.12 + public LogicalType logicalType() { + return LogicalType.Untyped; + } + @Override // since 2.9 public Boolean supportsUpdate(DeserializationConfig config) { // 21-Apr-2017, tatu: Bit tricky... some values, yes. So let's say "dunno" @@ -640,7 +701,7 @@ public class UntypedObjectDeserializer @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - switch (p.getCurrentTokenId()) { + switch (p.currentTokenId()) { case JsonTokenId.ID_START_OBJECT: { JsonToken t = p.nextToken(); @@ -703,7 +764,7 @@ public class UntypedObjectDeserializer @Override public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException { - switch (p.getCurrentTokenId()) { + switch (p.currentTokenId()) { case JsonTokenId.ID_START_ARRAY: case JsonTokenId.ID_START_OBJECT: case JsonTokenId.ID_FIELD_NAME: @@ -747,7 +808,7 @@ public class UntypedObjectDeserializer return deserialize(p, ctxt); } - switch (p.getCurrentTokenId()) { + switch (p.currentTokenId()) { case JsonTokenId.ID_END_OBJECT: case JsonTokenId.ID_END_ARRAY: return intoValue; @@ -881,18 +942,72 @@ public class UntypedObjectDeserializer if (key == null) { LinkedHashMap<String, Object> result = new LinkedHashMap<String, Object>(4); result.put(key1, value1); - result.put(key2, value2); + if (result.put(key2, value2) != null) { + // 22-May-2020, tatu: [databind#2733] may need extra handling + return _mapObjectWithDups(p, ctxt, result, key1, value1, value2, key); + } return result; } // And then the general case; default map size is 16 LinkedHashMap<String, Object> result = new LinkedHashMap<String, Object>(); result.put(key1, value1); - result.put(key2, value2); + if (result.put(key2, value2) != null) { + // 22-May-2020, tatu: [databind#2733] may need extra handling + return _mapObjectWithDups(p, ctxt, result, key1, value1, value2, key); + } + do { p.nextToken(); - result.put(key, deserialize(p, ctxt)); + final Object newValue = deserialize(p, ctxt); + final Object oldValue = result.put(key, newValue); + if (oldValue != null) { + return _mapObjectWithDups(p, ctxt, result, key, oldValue, newValue, + p.nextFieldName()); + } } while ((key = p.nextFieldName()) != null); return result; } + + // NOTE: copied from above (alas, no easy way to share/reuse) + // @since 2.12 (wrt [databind#2733] + protected Object _mapObjectWithDups(JsonParser p, DeserializationContext ctxt, + final Map<String, Object> result, String key, + Object oldValue, Object newValue, String nextKey) throws IOException + { + final boolean squashDups = ctxt.isEnabled(StreamReadCapability.DUPLICATE_PROPERTIES); + + if (squashDups) { + _squashDups(result, key, oldValue, newValue); + } + + while (nextKey != null) { + p.nextToken(); + newValue = deserialize(p, ctxt); + oldValue = result.put(nextKey, newValue); + if ((oldValue != null) && squashDups) { + _squashDups(result, key, oldValue, newValue); + } + nextKey = p.nextFieldName(); + } + + return result; + } + + // NOTE: copied from above (alas, no easy way to share/reuse) + @SuppressWarnings("unchecked") + private void _squashDups(final Map<String, Object> result, String key, + Object oldValue, Object newValue) + { + if (oldValue instanceof List<?>) { + ((List<Object>) oldValue).add(newValue); + result.put(key, oldValue); + } else { + ArrayList<Object> l = new ArrayList<>(); + l.add(oldValue); + l.add(newValue); + result.put(key, l); + } + } + } } |