diff options
146 files changed, 7098 insertions, 2443 deletions
diff --git a/.travis.yml b/.travis.yml index 0e9095c2e..4b1d45a5e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,8 @@ git: submodules: false # Since Jackson 2.10 build requires JDK 8 even if only JDK 7 runtime +# But we also want to test against newer LTS versions +# 08-Jul-2020, tatu: can not yet enable JDK14 due to new Record tests failing jdk: - openjdk8 - openjdk11 @@ -24,7 +26,7 @@ after_success: branches: only: - master - - "2.11" + - "2.12" env: global: @@ -1,16 +1,21 @@ <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <!-- This module was also published with a richer model, Gradle metadata, --> + <!-- which should be used instead. Do not delete the following line which --> + <!-- is to indicate to Gradle or any Gradle module metadata file consumer --> + <!-- that they should prefer consuming it instead. --> + <!-- do_not_remove: published-with-gradle-metadata --> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.fasterxml.jackson</groupId> <artifactId>jackson-base</artifactId> - <version>2.11.2-SNAPSHOT</version> + <version>2.12.0-SNAPSHOT</version> </parent> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> - <version>2.11.2-SNAPSHOT</version> + <version>2.12.0-SNAPSHOT</version> <name>jackson-databind</name> <packaging>bundle</packaging> <description>General data-binding functionality for Jackson: works on core streaming API</description> @@ -122,7 +127,6 @@ <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> - <version>0.8.4</version> <executions> <execution> <goals> @@ -177,8 +181,8 @@ <artifactId>maven-javadoc-plugin</artifactId> <configuration> <links combine.children="append"> - <link>http://fasterxml.github.com/jackson-annotations/javadoc/2.11</link> - <link>http://fasterxml.github.com/jackson-core/javadoc/2.11</link> + <link>http://fasterxml.github.com/jackson-annotations/javadoc/2.12</link> + <link>http://fasterxml.github.com/jackson-core/javadoc/2.12</link> </links> </configuration> </plugin> @@ -196,7 +200,12 @@ <groupId>org.moditect</groupId> <artifactId>moditect-maven-plugin</artifactId> </plugin> - </plugins> + + <plugin> + <groupId>de.jjohannes</groupId> + <artifactId>gradle-module-metadata-maven-plugin</artifactId> + </plugin> + </plugins> </build> <profiles> @@ -207,6 +216,57 @@ <skipTests>true</skipTests> </properties> </profile> + <profile> + <!-- Build Record tests using Java 14 if JDK is available --> + <id>java14+</id> + <activation> + <jdk>[14,</jdk> + </activation> + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <id>add-test-source</id> + <phase>generate-test-sources</phase> + <goals> + <goal>add-test-source</goal> + </goals> + <configuration> + <sources> + <source>src/test-jdk14/java</source> + </sources> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <inherited>true</inherited> + <configuration> + <optimize>true</optimize> + <!-- Enable Java 14+ for all sources so that Intellij picks the right language level --> + <source>14</source> + <release>14</release> + <compilerArgs> + <arg>-parameters</arg> + <arg>--enable-preview</arg> + </compilerArgs> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <argLine>--enable-preview</argLine> + </configuration> + </plugin> + </plugins> + </build> + </profile> </profiles> </project> diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index be405fbab..258b188e0 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -1069,6 +1069,8 @@ Ville Koskela (vjkoskela@github) (2.11.0) * Reported #2486: Builder Deserialization with JsonCreator Value vs Array (2.11.1) + * Contributed fix for #792: Deserialization Not Working Right with Generic Types and Builders + (2.12.0) Fitz (Joongsoo.Park) (joongsoo@github) * Contributed #2511: Add `SerializationFeature.WRITE_SELF_REFERENCES_AS_NULL` @@ -1144,3 +1146,39 @@ Joshua Shannon (retrodaredevil@github) Daniel Hrabovcak (TheSpiritXIII@github) * Reported #2796: `TypeFactory.constructType()` does not take `TypeBindings` correctly (2.11.2) + +Mike Gilbode (gilbode@github) + * Reported #792: Deserialization Not Working Right with Generic Types and Builders + (2.12.0) + +Patrick Jungermann (pjungermann@github) + * Requested #1852: Allow case insensitive deserialization of String value into + `boolean`/`Boolean` (esp for Excel) + (2.12.0) + +Nate Bauernfeind (nbauernfeind@github) + * Reported #2091: `ReferenceType` does not expose valid containedType + (2.12.0) + +Xiang Zhang (zhangyangyu@github) + * Reported #2118: `JsonProperty.Access.READ_ONLY` does not work with "getter-as-setter" + Collections + (2.12.0) + +Yona Appletree (Yona-Appletree@github) + * Reported #2283: `JsonProperty.Access.READ_ONLY` fails with collections when a + property name is specified + (2.12.0) + +David Bidorff (bidorffOL@github) + * Reported, contributed fix for #2719: `FAIL_ON_IGNORED_PROPERTIES` does not throw + on `READONLY` properties with an explicit name + (2.12.0) + +Jendrik Johannes (jjohannes@github) + * Contributed #2726: Add Gradle Module Metadata for version alignment with Gradle 6 + (2.12.0) + +Swayam Raina (swayamraina@github) + * Contributed #2761: Support multiple names in `JsonSubType.Type` + (2.12.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index d1182d17f..6039a2f07 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -4,6 +4,54 @@ Project: jackson-databind === Releases === ------------------------------------------------------------------------ +2.12.0 (not yet released) + +#426: `@JsonIgnoreProperties` does not prevent Exception Conflicting getter/setter + definitions for property + (reported by gmkll@github) +#921: Deserialization Not Working Right with Generic Types and Builders + (reported by Mike G; fix contributed by Ville K) +#1852: Allow case insensitive deserialization of String value into + `boolean`/`Boolean` (esp for Excel) + (requested by Patrick J) +#1886: Allow use of `@JsonFormat(with=JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)` + on Class +#1919: Abstract class included as part of known type ids for error message + when using JsonSubTypes + (reported by Incara@github) +#2066: Distinguish null from empty string for UUID deserialization + (requested by leonshaw@github) +#2091: `ReferenceType` does not expose valid containedType + (reported by Nate B) +#2113: Add `CoercionConfig[s]` mechanism for configuring allowed coercions +#2118: `JsonProperty.Access.READ_ONLY` does not work with "getter-as-setter" `Collection`s + (reported by Xiang Z) +#2283: `JsonProperty.Access.READ_ONLY` fails with collections when a property name is specified + (reported by Yona A) +#2644: `BigDecimal` precision not retained for polymorphic deserialization + (reported by rost5000@github) +#2675: Support use of `Void` valued properties (`MapperFeature.ALLOW_VOID_VALUED_PROPERTIES`) +#2683: Explicitly fail (de)serialization of `java.time.*` types in absence of + registered custom (de)serializers +#2707: Improve description included in by `DeserializationContext.handleUnexpectedToken()` +#2719: `FAIL_ON_IGNORED_PROPERTIES` does not throw on `READONLY` properties with + an explicit name + (reported, fix contributed by David B) +#2726: Add Gradle Module Metadata for version alignment with Gradle 6 + (contributed by Jendrik J) +#2732: Allow `JsonNode` auto-convert into `ArrayNode` if duplicates found (for XML) +#2733: Allow values of "untyped" auto-convert into `List` if duplicates found (for XML) +#2751: Add `ValueInstantiator.createContextual(...) +#2761: Support multiple names in `JsonSubType.Type` + (contributed by Swayam R) +#2776: Explicitly fail (de)serialization of `org.joda.time.*` types in absence of registered + custom (de)serializers +#2784: Trailing zeros are stripped when deserializing BigDecimal values inside a + @JsonUnwrapped property + (reported by mjustin@github) +- Add `BeanDeserializerBase.isCaseInsensitive()` +- Some refactoring of `CollectionDeserializer` to solve CSV array handling issues + 2.11.2 (not yet released) #2783: Parser/Generator features not set when using `ObjectMapper.createParser()`, diff --git a/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java b/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java index d0401ddd9..d8035f031 100644 --- a/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java +++ b/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java @@ -101,6 +101,8 @@ public abstract class AnnotationIntrospector * Factory method for accessing "no operation" implementation * of introspector: instance that will never find any annotation-based * configuration. + * + * @return "no operation" instance */ public static AnnotationIntrospector nopInstance() { return NopAnnotationIntrospector.instance; @@ -126,6 +128,9 @@ public abstract class AnnotationIntrospector * as contents. * This usually works for sub-classes, except for proxy or delegating "container * introspectors" which need to override implementation. + * + * @return Collection of all introspectors starting with this one, in case + * multiple introspectors are chained */ public Collection<AnnotationIntrospector> allIntrospectors() { return Collections.singletonList(this); @@ -140,6 +145,11 @@ public abstract class AnnotationIntrospector * Default implementation adds this introspector in result; this usually * works for sub-classes, except for proxy or delegating "container * introspectors" which need to override implementation. + * + * @param result Container to add introspectors to + * + * @return Passed in {@code Collection} filled with introspectors as explained + * above */ public Collection<AnnotationIntrospector> allIntrospectors(Collection<AnnotationIntrospector> result) { result.add(this); @@ -165,8 +175,11 @@ public abstract class AnnotationIntrospector * Method for checking whether given annotation is considered an * annotation bundle: if so, all meta-annotations it has will * be used instead of annotation ("bundle") itself. - * - * @since 2.0 + * + * @param ann Annotated entity to introspect + * + * @return True if given annotation is considered an annotation + * bundle; false if not */ public boolean isAnnotationBundle(Annotation ann) { return false; @@ -185,8 +198,11 @@ public abstract class AnnotationIntrospector * values referenced by annotated property; latter * having precedence) should include Object Identifier, * and if so, specify details of Object Identity used. + * + * @param ann Annotated entity to introspect * - * @since 2.0 + * @return Details of Object Id as explained above, if Object Id + * handling to be applied; {@code null} otherwise. */ public ObjectIdInfo findObjectIdInfo(Annotated ann) { return null; @@ -194,6 +210,11 @@ public abstract class AnnotationIntrospector /** * Method for figuring out additional properties of an Object Identity reference + * + * @param ann Annotated entity to introspect + * @param objectIdInfo (optional) Base Object Id information, if any; {@code null} if none + * + * @return {@link ObjectIdInfo} augmented with possible additional information * * @since 2.1 */ @@ -216,6 +237,10 @@ public abstract class AnnotationIntrospector *<p> * NOTE: method signature changed in 2.1, to return {@link PropertyName} * instead of String. + * + * @param ac Annotated class to introspect + * + * @return Root name to use, if any; {@code null} if not */ public PropertyName findRootName(AnnotatedClass ac) { return null; @@ -229,6 +254,8 @@ public abstract class AnnotationIntrospector * {@link #findPropertiesToIgnore(Annotated, boolean)} and * {@link #findIgnoreUnknownProperties(AnnotatedClass)}. * + * @param ac Annotated class to introspect + * * @since 2.8 */ public JsonIgnoreProperties.Value findPropertyIgnorals(Annotated ac) @@ -243,7 +270,7 @@ public abstract class AnnotationIntrospector * (class, not generics aware) should be completely ignored for * serialization and deserialization purposes. * - * @param ac Type to check + * @param ac Annotated class to introspect * * @return Boolean.TRUE if properties of type should be ignored; * Boolean.FALSE if they are not to be ignored, null for default @@ -254,6 +281,8 @@ public abstract class AnnotationIntrospector /** * Method for finding if annotated class has associated filter; and if so, * to return id that is used to locate filter. + * + * @param ann Annotated entity to introspect * * @return Id of the filter to use for filtering properties of annotated * class, if any; or null if none found. @@ -265,7 +294,9 @@ public abstract class AnnotationIntrospector * class, if any specified by annotations; and if so, either return * a {@link PropertyNamingStrategy} instance, or Class to use for * creating instance - * + * + * @param ac Annotated class to introspect + * * @return Sub-class or instance of {@link PropertyNamingStrategy}, if one * is specified for given class; null if not. * @@ -279,6 +310,8 @@ public abstract class AnnotationIntrospector * There are no further definitions for contents; for example, whether * these may be marked up using HTML (or something like wiki format like Markup) * is not defined. + * + * @param ac Annotated class to introspect * * @return Human-readable description, if any. * @@ -289,7 +322,10 @@ public abstract class AnnotationIntrospector /** * @param forSerialization True if requesting properties to ignore for serialization; * false if for deserialization - * + * @param ac Annotated class to introspect + * + * @return Array of names of properties to ignore + * * @since 2.6 * * @deprecated Since 2.8, use {@link #findPropertyIgnorals} instead @@ -300,6 +336,10 @@ public abstract class AnnotationIntrospector } /** + * @param ac Annotated class to introspect + * + * @return Array of names of properties to ignore + * * @deprecated Since 2.6, use variant that takes second argument. */ @Deprecated // since 2.6 @@ -309,6 +349,11 @@ public abstract class AnnotationIntrospector /** * Method for checking whether an annotation indicates that all unknown properties + * should be ignored. + * + * @param ac Annotated class to introspect + * + * @return True if class has something indicating "ignore [all] unknown properties" * * @deprecated Since 2.8, use {@link #findPropertyIgnorals} instead */ @@ -941,16 +986,18 @@ public abstract class AnnotationIntrospector * method to be used for accessing set of miscellaneous "extra" * properties, often bound with matching "any setter" method. * + * @param ann Annotated entity to check + * * @return True if such annotation is found (and is not disabled), * false otherwise * * @since 2.9 */ - public Boolean hasAnyGetter(Annotated a) { + public Boolean hasAnyGetter(Annotated ann) { // 21-Nov-2016, tatu: Delegate in 2.9; remove redirect from later versions - if (a instanceof AnnotatedMethod) { - if (hasAnyGetterAnnotation((AnnotatedMethod) a)) { + if (ann instanceof AnnotatedMethod) { + if (hasAnyGetterAnnotation((AnnotatedMethod) ann)) { return true; } } @@ -962,6 +1009,13 @@ public abstract class AnnotationIntrospector * have explicitly defined name. Method will overwrite entries in incoming <code>names</code> * array with explicit names found, if any, leaving other entries unmodified. * + * @param enumType Type of Enumeration + * @param enumValues Values of enumeration + * @param names Matching declared names of enumeration values (with indexes + * matching {@code enumValues} entries) + * + * @return Array of names to use (possible {@code names} passed as argument) + * * @since 2.7 */ public String[] findEnumValues(Class<?> enumType, Enum<?>[] enumValues, String[] names) { @@ -976,6 +1030,11 @@ public abstract class AnnotationIntrospector * If so, these aliases should be returned in {@code aliases} {@link List} passed * as argument (and initialized for proper size by caller). * + * @param enumType Type of Enumeration + * @param enumValues Values of enumeration + * @param aliases (in/out) Pre-allocated array where aliases found, if any, may be + * added (in indexes matching those of {@code enumValues}) + * * @since 2.11 */ public void findEnumAliases(Class<?> enumType, Enum<?>[] enumValues, String[][] aliases) { @@ -985,8 +1044,9 @@ public abstract class AnnotationIntrospector /** * Finds the Enum value that should be considered the default value, if possible. * - * @param enumCls The Enum class to scan for the default value. - * @return null if none found or it's not possible to determine one. + * @param enumCls The Enum class to scan for the default value + * + * @return null if none found or it's not possible to determine one * * @since 2.8 */ @@ -999,6 +1059,8 @@ public abstract class AnnotationIntrospector * given enumeration entry; used when serializing enumerations * as Strings (the standard method). * + * @param value Enum value to introspect + * * @return Serialized enum value. * * @deprecated Since 2.8: use {@link #findEnumValues} instead because this method @@ -1012,6 +1074,8 @@ public abstract class AnnotationIntrospector } /** + * @param am Annotated method to check + * * @deprecated Since 2.9 Use {@link #hasAsValue(Annotated)} instead. */ @Deprecated // since 2.9 @@ -1020,6 +1084,8 @@ public abstract class AnnotationIntrospector } /** + * @param am Annotated method to check + * * @deprecated Since 2.9 Use {@link #hasAnyGetter} instead */ @Deprecated @@ -1142,6 +1208,7 @@ public abstract class AnnotationIntrospector * Type is usually narrowing conversion (i.e.subtype of declared type). * Declared return type of the method is also considered acceptable. * + * @param ann Annotated entity to introspect * @param baseType Assumed type before considering annotations * * @return Class to use for deserialization instead of declared type @@ -1149,7 +1216,7 @@ public abstract class AnnotationIntrospector * @deprecated Since 2.7 call {@link #refineDeserializationType} instead */ @Deprecated - public Class<?> findDeserializationType(Annotated am, JavaType baseType) { + public Class<?> findDeserializationType(Annotated ann, JavaType baseType) { return null; } @@ -1158,6 +1225,7 @@ public abstract class AnnotationIntrospector * method can have, to define more specific key type to use. * It should be only be used with {@link java.util.Map} types. * + * @param ann Annotated entity to introspect * @param baseKeyType Assumed key type before considering annotations * * @return Class specifying more specific type to use instead of @@ -1166,7 +1234,7 @@ public abstract class AnnotationIntrospector * @deprecated Since 2.7 call {@link #refineDeserializationType} instead */ @Deprecated - public Class<?> findDeserializationKeyType(Annotated am, JavaType baseKeyType) { + public Class<?> findDeserializationKeyType(Annotated ann, JavaType baseKeyType) { return null; } @@ -1176,6 +1244,7 @@ public abstract class AnnotationIntrospector * content refers to Map values and Collection/array elements. * It should be only be used with Map, Collection and array types. * + * @param ann Annotated entity to introspect * @param baseContentType Assumed content (value) type before considering annotations * * @return Class specifying more specific type to use instead of @@ -1184,7 +1253,7 @@ public abstract class AnnotationIntrospector * @deprecated Since 2.7 call {@link #refineDeserializationType} instead */ @Deprecated - public Class<?> findDeserializationContentType(Annotated am, JavaType baseContentType) { + public Class<?> findDeserializationContentType(Annotated ann, JavaType baseContentType) { return null; } @@ -1198,6 +1267,8 @@ public abstract class AnnotationIntrospector * Method getting {@link ValueInstantiator} to use for given * type (class): return value can either be an instance of * instantiator, or class of instantiator to create. + * + * @param ac Annotated class to introspect */ public Object findValueInstantiator(AnnotatedClass ac) { return null; @@ -1213,6 +1284,8 @@ public abstract class AnnotationIntrospector * method does not allow returning instances: the reason is * that builders have state, and a separate instance needs * to be created for each deserialization call. + * + * @param ac Annotated class to introspect * * @since 2.0 */ @@ -1221,6 +1294,8 @@ public abstract class AnnotationIntrospector } /** + * @param ac Annotated class to introspect + * * @since 2.0 */ public JsonPOJOBuilder.Value findPOJOBuilderConfig(AnnotatedClass ac) { @@ -1241,13 +1316,13 @@ public abstract class AnnotationIntrospector * is found; otherwise a non-null name (possibly * {@link PropertyName#USE_DEFAULT}, which means "use default heuristics"). * - * @param a Property accessor to check - * + * @param ann Annotated entity to check + * * @return Name to use if found; null if not. - * + * * @since 2.1 */ - public PropertyName findNameForDeserialization(Annotated a) { + public PropertyName findNameForDeserialization(Annotated ann) { return null; } @@ -1256,32 +1331,38 @@ public abstract class AnnotationIntrospector * that suggests that the method is to serve as "any setter"; * method to be used for setting values of any properties for * which no dedicated setter method is found. + * + * @param ann Annotated entity to check * * @return True if such annotation is found (and is not disabled), * false otherwise - * + * * @since 2.9 */ - public Boolean hasAnySetter(Annotated a) { + public Boolean hasAnySetter(Annotated ann) { return null; } /** * Method for finding possible settings for property, given annotations * on an accessor. + * + * @param ann Annotated entity to check * * @since 2.9 */ - public JsonSetter.Value findSetterInfo(Annotated a) { + public JsonSetter.Value findSetterInfo(Annotated ann) { return JsonSetter.Value.empty(); } /** * Method for finding merge settings for property, if any. + * + * @param ann Annotated entity to check * * @since 2.9 */ - public Boolean findMergeInfo(Annotated a) { + public Boolean findMergeInfo(Annotated ann) { return null; } @@ -1296,15 +1377,15 @@ public abstract class AnnotationIntrospector * as well as possibly as when using mix-in annotations. * * @param config Configuration settings in effect (for serialization or deserialization) - * @param a Annotated accessor (usually constructor or static method) to check + * @param ann Annotated accessor (usually constructor or static method) to check * * @since 2.9 */ - public JsonCreator.Mode findCreatorAnnotation(MapperConfig<?> config, Annotated a) { + public JsonCreator.Mode findCreatorAnnotation(MapperConfig<?> config, Annotated ann) { // 13-Sep-2016, tatu: for backwards compatibility, implement using delegation /// (remove from version AFTER 2.9) - if (hasCreatorAnnotation(a)) { - JsonCreator.Mode mode = findCreatorBinding(a); + if (hasCreatorAnnotation(ann)) { + JsonCreator.Mode mode = findCreatorBinding(ann); if (mode == null) { mode = JsonCreator.Mode.DEFAULT; } @@ -1319,6 +1400,8 @@ public abstract class AnnotationIntrospector * that suggests that the method is a "creator" (aka factory) * method to be used for construct new instances of deserialized * values. + * + * @param ann Annotated entity to check * * @return True if such annotation is found (and is not disabled), * false otherwise @@ -1326,7 +1409,7 @@ public abstract class AnnotationIntrospector * @deprecated Since 2.9 use {@link #findCreatorAnnotation} instead. */ @Deprecated - public boolean hasCreatorAnnotation(Annotated a) { + public boolean hasCreatorAnnotation(Annotated ann) { return false; } @@ -1336,16 +1419,22 @@ public abstract class AnnotationIntrospector * true), for cases where there may be ambiguity (currently: single-argument * creator with implicit but no explicit name for the argument). * + * @param ann Annotated entity to check + * * @since 2.5 * @deprecated Since 2.9 use {@link #findCreatorAnnotation} instead. */ @Deprecated - public JsonCreator.Mode findCreatorBinding(Annotated a) { + public JsonCreator.Mode findCreatorBinding(Annotated ann) { return null; } /** + * @param am Annotated method to check + * * @deprecated Since 2.9 use {@link #hasAnySetter} instead. + * + * @return {@code true} if "any-setter" annotation was found; {@code false} otherwise */ @Deprecated // since 2.9 public boolean hasAnySetterAnnotation(AnnotatedMethod am) { @@ -1370,12 +1459,18 @@ public abstract class AnnotationIntrospector *<code> * return annotated.getAnnotation(annoClass); *</code> - * + * + * @param ann Annotated entity to check for specified annotation + * @param annoClass Type of annotation to find + * + * @return Value of given annotation (as per {@code annoClass}), if entity + * has one; {@code null} otherwise + * * @since 2.5 */ - protected <A extends Annotation> A _findAnnotation(Annotated annotated, + protected <A extends Annotation> A _findAnnotation(Annotated ann, Class<A> annoClass) { - return annotated.getAnnotation(annoClass); + return ann.getAnnotation(annoClass); } /** @@ -1388,20 +1483,31 @@ public abstract class AnnotationIntrospector *<code> * return annotated.hasAnnotation(annoClass); *</code> + * + * @param ann Annotated entity to check for specified annotation + * @param annoClass Type of annotation to find + * + * @return {@code true} if specified annotation exists in given entity; {@code false} if not * * @since 2.5 */ - protected boolean _hasAnnotation(Annotated annotated, Class<? extends Annotation> annoClass) { - return annotated.hasAnnotation(annoClass); + protected boolean _hasAnnotation(Annotated ann, Class<? extends Annotation> annoClass) { + return ann.hasAnnotation(annoClass); } /** * Alternative lookup method that is used to see if annotation has at least one of * annotations of types listed in second argument. * + * @param ann Annotated entity to check for specified annotation + * @param annoClasses Types of annotation to find + * + * @return {@code true} if at least one of specified annotation exists in given entity; + * {@code false} otherwise + * * @since 2.7 */ - protected boolean _hasOneOf(Annotated annotated, Class<? extends Annotation>[] annoClasses) { - return annotated.hasOneOf(annoClasses); + protected boolean _hasOneOf(Annotated ann, Class<? extends Annotation>[] annoClasses) { + return ann.hasOneOf(annoClasses); } } diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java index 08977d36e..1458023ad 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler; import com.fasterxml.jackson.databind.introspect.*; import com.fasterxml.jackson.databind.jsontype.*; import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.LinkedNode; import com.fasterxml.jackson.databind.util.RootNameLookup; @@ -49,6 +50,11 @@ public final class DeserializationConfig */ protected final JsonNodeFactory _nodeFactory; + /** + * @since 2.12 + */ + protected final CoercionConfigs _coercionConfigs; + /* /********************************************************** /* Deserialization features @@ -98,15 +104,19 @@ public final class DeserializationConfig /** * Constructor used by ObjectMapper to create default configuration object instance. + * + * @since 2.12 */ public DeserializationConfig(BaseSettings base, SubtypeResolver str, SimpleMixInResolver mixins, RootNameLookup rootNames, - ConfigOverrides configOverrides) + ConfigOverrides configOverrides, + CoercionConfigs coercionConfigs) { super(base, str, mixins, rootNames, configOverrides); _deserFeatures = DESER_FEATURE_DEFAULTS; - _nodeFactory = JsonNodeFactory.instance; _problemHandlers = null; + _nodeFactory = JsonNodeFactory.instance; + _coercionConfigs = coercionConfigs; _parserFeatures = 0; _parserFeaturesToChange = 0; _formatReadFeatures = 0; @@ -116,32 +126,36 @@ public final class DeserializationConfig /** * Copy-constructor used for making a copy used by new {@link ObjectMapper}. * - * @since 2.11.2 + * @since 2.12 */ protected DeserializationConfig(DeserializationConfig src, SubtypeResolver str, SimpleMixInResolver mixins, RootNameLookup rootNames, - ConfigOverrides configOverrides) + ConfigOverrides configOverrides, + CoercionConfigs coercionConfigs) { super(src, str, mixins, rootNames, configOverrides); _deserFeatures = src._deserFeatures; _problemHandlers = src._problemHandlers; _nodeFactory = src._nodeFactory; + _coercionConfigs = coercionConfigs; _parserFeatures = src._parserFeatures; _parserFeaturesToChange = src._parserFeaturesToChange; _formatReadFeatures = src._formatReadFeatures; _formatReadFeaturesToChange = src._formatReadFeaturesToChange; } - /** - * @since 2.9 - * @deprecated since 2.11.2 - */ - @Deprecated + @Deprecated // since 2.12, remove from 2.13 or later + public DeserializationConfig(BaseSettings base, + SubtypeResolver str, SimpleMixInResolver mixins, RootNameLookup rootNames, + ConfigOverrides configOverrides) { + this(base, str, mixins, rootNames, configOverrides, new CoercionConfigs()); + } + + @Deprecated // since 2.11.2, remove from 2.13 or later protected DeserializationConfig(DeserializationConfig src, SimpleMixInResolver mixins, RootNameLookup rootNames, - ConfigOverrides configOverrides) - { - this(src, src._subtypeResolver, mixins, rootNames, configOverrides); + ConfigOverrides configOverrides) { + this(src, src._subtypeResolver, mixins, rootNames, configOverrides, new CoercionConfigs()); } /* @@ -158,14 +172,15 @@ public final class DeserializationConfig { super(src, mapperFeatures); _deserFeatures = deserFeatures; - _nodeFactory = src._nodeFactory; _problemHandlers = src._problemHandlers; + _nodeFactory = src._nodeFactory; + _coercionConfigs = src._coercionConfigs; _parserFeatures = parserFeatures; _parserFeaturesToChange = parserFeatureMask; _formatReadFeatures = formatFeatures; _formatReadFeaturesToChange = formatFeatureMask; } - + /** * Copy constructor used to create a non-shared instance with given mix-in * annotation definitions and subtype resolver. @@ -174,8 +189,9 @@ public final class DeserializationConfig { super(src, str); _deserFeatures = src._deserFeatures; - _nodeFactory = src._nodeFactory; _problemHandlers = src._problemHandlers; + _nodeFactory = src._nodeFactory; + _coercionConfigs = src._coercionConfigs; _parserFeatures = src._parserFeatures; _parserFeaturesToChange = src._parserFeaturesToChange; _formatReadFeatures = src._formatReadFeatures; @@ -186,8 +202,9 @@ public final class DeserializationConfig { super(src, base); _deserFeatures = src._deserFeatures; - _nodeFactory = src._nodeFactory; _problemHandlers = src._problemHandlers; + _nodeFactory = src._nodeFactory; + _coercionConfigs = src._coercionConfigs; _parserFeatures = src._parserFeatures; _parserFeaturesToChange = src._parserFeaturesToChange; _formatReadFeatures = src._formatReadFeatures; @@ -200,6 +217,7 @@ public final class DeserializationConfig _deserFeatures = src._deserFeatures; _problemHandlers = src._problemHandlers; _nodeFactory = f; + _coercionConfigs = src._coercionConfigs; _parserFeatures = src._parserFeatures; _parserFeaturesToChange = src._parserFeaturesToChange; _formatReadFeatures = src._formatReadFeatures; @@ -213,6 +231,7 @@ public final class DeserializationConfig _deserFeatures = src._deserFeatures; _problemHandlers = problemHandlers; _nodeFactory = src._nodeFactory; + _coercionConfigs = src._coercionConfigs; _parserFeatures = src._parserFeatures; _parserFeaturesToChange = src._parserFeaturesToChange; _formatReadFeatures = src._formatReadFeatures; @@ -225,6 +244,7 @@ public final class DeserializationConfig _deserFeatures = src._deserFeatures; _problemHandlers = src._problemHandlers; _nodeFactory = src._nodeFactory; + _coercionConfigs = src._coercionConfigs; _parserFeatures = src._parserFeatures; _parserFeaturesToChange = src._parserFeaturesToChange; _formatReadFeatures = src._formatReadFeatures; @@ -237,6 +257,7 @@ public final class DeserializationConfig _deserFeatures = src._deserFeatures; _problemHandlers = src._problemHandlers; _nodeFactory = src._nodeFactory; + _coercionConfigs = src._coercionConfigs; _parserFeatures = src._parserFeatures; _parserFeaturesToChange = src._parserFeaturesToChange; _formatReadFeatures = src._formatReadFeatures; @@ -249,6 +270,7 @@ public final class DeserializationConfig _deserFeatures = src._deserFeatures; _problemHandlers = src._problemHandlers; _nodeFactory = src._nodeFactory; + _coercionConfigs = src._coercionConfigs; _parserFeatures = src._parserFeatures; _parserFeaturesToChange = src._parserFeaturesToChange; _formatReadFeatures = src._formatReadFeatures; @@ -261,6 +283,7 @@ public final class DeserializationConfig _deserFeatures = src._deserFeatures; _problemHandlers = src._problemHandlers; _nodeFactory = src._nodeFactory; + _coercionConfigs = src._coercionConfigs; _parserFeatures = src._parserFeatures; _parserFeaturesToChange = src._parserFeaturesToChange; _formatReadFeatures = src._formatReadFeatures; @@ -705,13 +728,30 @@ public final class DeserializationConfig * * @since 2.5 */ - public void initialize(JsonParser p) { + public JsonParser initialize(JsonParser p) { + if (_parserFeaturesToChange != 0) { + p.overrideStdFeatures(_parserFeatures, _parserFeaturesToChange); + } + if (_formatReadFeaturesToChange != 0) { + p.overrideFormatFeatures(_formatReadFeatures, _formatReadFeaturesToChange); + } + return p; + } + + /** + * @since 2.12 + */ + public JsonParser initialize(JsonParser p, FormatSchema schema) { if (_parserFeaturesToChange != 0) { p.overrideStdFeatures(_parserFeatures, _parserFeaturesToChange); } if (_formatReadFeaturesToChange != 0) { p.overrideFormatFeatures(_formatReadFeatures, _formatReadFeaturesToChange); } + if (schema != null) { + p.setSchema(schema); + } + return p; } /* @@ -867,4 +907,53 @@ public final class DeserializationConfig } return b.buildTypeDeserializer(this, baseType, subtypes); } + + /* + /********************************************************************** + /* CoercionConfig access + /********************************************************************** + */ + + /** + * General-purpose accessor for finding what to do when specified coercion + * from shape that is now always allowed to be coerced from is requested. + * + * @param targetType Logical target type of coercion + * @param targetClass Physical target type of coercion + * @param inputShape Input shape to coerce from + * + * @return CoercionAction configured for specific coercion + * + * @since 2.12 + */ + public CoercionAction findCoercionAction(LogicalType targetType, + Class<?> targetClass, CoercionInputShape inputShape) + { + return _coercionConfigs.findCoercion(this, + targetType, targetClass, inputShape); + } + + /** + * More specialized accessor called in case of input being a blank + * String (one consisting of only white space characters with length of at least one). + * Will basically first determine if "blank as empty" is allowed: if not, + * returns {@code actionIfBlankNotAllowed}, otherwise returns action for + * {@link CoercionInputShape#EmptyString}. + * + * @param targetType Logical target type of coercion + * @param targetClass Physical target type of coercion + * @param actionIfBlankNotAllowed Return value to use in case "blanks as empty" + * is not allowed + * + * @return CoercionAction configured for specified coercion from blank string + * + * @since 2.12 + */ + public CoercionAction findCoercionFromBlankString(LogicalType targetType, + Class<?> targetClass, + CoercionAction actionIfBlankNotAllowed) + { + return _coercionConfigs.findCoercionFromBlankString(this, + targetType, targetClass, actionIfBlankNotAllowed); + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java index 275cbcba4..438886fdd 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java @@ -11,7 +11,9 @@ import com.fasterxml.jackson.annotation.ObjectIdGenerator; import com.fasterxml.jackson.annotation.ObjectIdResolver; import com.fasterxml.jackson.core.*; - +import com.fasterxml.jackson.core.util.JacksonFeatureSet; +import com.fasterxml.jackson.databind.cfg.CoercionAction; +import com.fasterxml.jackson.databind.cfg.CoercionInputShape; import com.fasterxml.jackson.databind.cfg.ContextAttributes; import com.fasterxml.jackson.databind.deser.*; import com.fasterxml.jackson.databind.deser.impl.ObjectIdReader; @@ -29,6 +31,7 @@ import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.databind.util.*; @@ -97,6 +100,13 @@ public abstract class DeserializationContext protected final int _featureFlags; /** + * Capabilities of the input format. + * + * @since 2.12 + */ + protected final JacksonFeatureSet<StreamReadCapability> _readCapabilities; + + /** * Currently active view, if any. */ protected final Class<?> _view; @@ -164,6 +174,7 @@ public abstract class DeserializationContext } _cache = cache; _featureFlags = 0; + _readCapabilities = null; _config = null; _injectableValues = null; _view = null; @@ -178,6 +189,7 @@ public abstract class DeserializationContext _config = src._config; _featureFlags = src._featureFlags; + _readCapabilities = src._readCapabilities; _view = src._view; _parser = src._parser; _injectableValues = src._injectableValues; @@ -193,7 +205,11 @@ public abstract class DeserializationContext { _cache = src._cache; _factory = src._factory; - + // 08-Jun-2020. tatu: Called only for `ObjectMapper.canDeserialize()` + // (see [databind#2749]), not sure what's the best work-around but + // start with this: + _readCapabilities = (p == null) ? null : p.getReadCapabilities(); + _config = config; _featureFlags = config.getDeserializationFeatures(); _view = config.getActiveView(); @@ -203,6 +219,27 @@ public abstract class DeserializationContext } /** + * Constructor used for creating bogus per-call style instance, but + * without underlying parser: needed for deserializer pre-fetching + * + * @since 2.12 + */ + protected DeserializationContext(DeserializationContext src, + DeserializationConfig config) + { + _cache = src._cache; + _factory = src._factory; + _readCapabilities = null; + + _config = config; + _featureFlags = config.getDeserializationFeatures(); + _view = null; + _parser = null; + _injectableValues = null; + _attributes = null; + } + + /** * Copy-constructor for use with <code>copy()</code> by {@link ObjectMapper#copy()} */ protected DeserializationContext(DeserializationContext src) { @@ -211,6 +248,7 @@ public abstract class DeserializationContext _config = src._config; _featureFlags = src._featureFlags; + _readCapabilities = src._readCapabilities; _view = src._view; _injectableValues = null; } @@ -323,7 +361,7 @@ public abstract class DeserializationContext /* /********************************************************** - /* Public API, config setting accessors + /* Public API, config accessors /********************************************************** */ @@ -346,6 +384,18 @@ public abstract class DeserializationContext } /** + * Accessor for checking whether input format has specified capability + * or not. + * + * @return True if input format has specified capability; false if not + * + * @since 2.12 + */ + public final boolean isEnabled(StreamReadCapability cap) { + return _readCapabilities.isEnabled(cap); + } + + /** * Bulk access method for getting the bit mask of all {@link DeserializationFeature}s * that are enabled. * @@ -419,6 +469,53 @@ public abstract class DeserializationContext } /* + /********************************************************************** + /* Public API, CoercionConfig access (2.12+) + /********************************************************************** + */ + + /** + * General-purpose accessor for finding what to do when specified coercion + * from shape that is now always allowed to be coerced from is requested. + * + * @param targetType Logical target type of coercion + * @param targetClass Physical target type of coercion + * @param inputShape Input shape to coerce from + * + * @return CoercionAction configured for specific coercion + * + * @since 2.12 + */ + public CoercionAction findCoercionAction(LogicalType targetType, + Class<?> targetClass, CoercionInputShape inputShape) + { + return _config.findCoercionAction(targetType, targetClass, inputShape); + } + + /** + * More specialized accessor called in case of input being a blank + * String (one consisting of only white space characters with length of at least one). + * Will basically first determine if "blank as empty" is allowed: if not, + * returns {@code actionIfBlankNotAllowed}, otherwise returns action for + * {@link CoercionInputShape#EmptyString}. + * + * @param targetType Logical target type of coercion + * @param targetClass Physical target type of coercion + * @param actionIfBlankNotAllowed Return value to use in case "blanks as empty" + * is not allowed + * + * @return CoercionAction configured for specified coercion from blank string + * + * @since 2.12 + */ + public CoercionAction findCoercionFromBlankString(LogicalType targetType, + Class<?> targetClass, + CoercionAction actionIfBlankNotAllowed) + { + return _config.findCoercionFromBlankString(targetType, targetClass, actionIfBlankNotAllowed); + } + + /* /********************************************************** /* Public API, pass-through to DeserializerCache /********************************************************** @@ -428,7 +525,10 @@ public abstract class DeserializationContext * Method for checking whether we could find a deserializer * for given type. * - * @param type + * @param type Type to check + * @param cause (optional) Reference set to root cause if no deserializer + * could be found due to exception (to find the reason for failure) + * * @since 2.3 */ public boolean hasValueDeserializerFor(JavaType type, AtomicReference<Throwable> cause) { @@ -720,7 +820,7 @@ public abstract class DeserializationContext public Date parseDate(String dateStr) throws IllegalArgumentException { try { - DateFormat df = getDateFormat(); + DateFormat df = _getDateFormat(); return df.parse(dateStr); } catch (ParseException e) { throw new IllegalArgumentException(String.format( @@ -742,6 +842,40 @@ public abstract class DeserializationContext /* /********************************************************** + /* Extension points for more esoteric data coercion (2.12) + /********************************************************** + */ + + /** + * Method to call in case incoming shape is Object Value (and parser thereby + * points to {@link com.fasterxml.jackson.core.JsonToken#START_OBJECT} token), + * but a Scalar value (potentially coercible from String value) is expected. + * This would typically be used to deserializer a Number, Boolean value or some other + * "simple" unstructured value type. + * + * @param p Actual parser to read content from + * @param deser Deserializer that needs extracted String value + * @param scalarType Immediate type of scalar to extract; usually type deserializer + * handles but not always (for example, deserializer for {@code int[]} would pass + * scalar type of {@code int}) + * + * @return String value found; not {@code null} (exception should be thrown if no suitable + * value found) + * + * @throws IOException If there are problems either reading content (underlying parser + * problem) or finding expected scalar value + */ + public String extractScalarFromObject(JsonParser p, JsonDeserializer<?> deser, + Class<?> scalarType) + throws IOException + { + return reportInputMismatch(scalarType, String.format( +"Cannot deserialize value of type %s from %s (token `JsonToken.START_OBJECT`)", +ClassUtil.getClassDescription(scalarType), _shapeForToken(JsonToken.START_OBJECT))); + } + + /* + /********************************************************** /* Convenience methods for reading parsed values /********************************************************** */ @@ -1145,7 +1279,7 @@ public abstract class DeserializationContext public Object handleUnexpectedToken(Class<?> instClass, JsonParser p) throws IOException { - return handleUnexpectedToken(constructType(instClass), p.getCurrentToken(), p, null); + return handleUnexpectedToken(constructType(instClass), p.currentToken(), p, null); } /** @@ -1187,7 +1321,7 @@ public abstract class DeserializationContext public Object handleUnexpectedToken(JavaType targetType, JsonParser p) throws IOException { - return handleUnexpectedToken(targetType, p.getCurrentToken(), p, null); + return handleUnexpectedToken(targetType, p.currentToken(), p, null); } /** @@ -1220,19 +1354,20 @@ public abstract class DeserializationContext } reportBadDefinition(targetType, String.format( "DeserializationProblemHandler.handleUnexpectedToken() for type %s returned value of type %s", - ClassUtil.getClassDescription(targetType), + ClassUtil.getTypeDescription(targetType), ClassUtil.classNameOf(instance) )); } h = h.next(); } if (msg == null) { + final String targetDesc = ClassUtil.getTypeDescription(targetType); if (t == null) { - msg = String.format("Unexpected end-of-input when binding data into %s", - ClassUtil.getTypeDescription(targetType)); + msg = String.format("Unexpected end-of-input when trying read value of type %s", + targetDesc); } else { - msg = String.format("Cannot deserialize instance of %s out of %s token", - ClassUtil.getTypeDescription(targetType), t); + msg = String.format("Cannot deserialize value of type %s from %s (token `JsonToken.%s`)", + targetDesc, _shapeForToken(t), t); } } // 18-Jun-2020, tatu: to resolve [databind#2770], force access to `getText()` for scalars @@ -1645,7 +1780,7 @@ trailingToken, ClassUtil.nameOf(targetType) JsonToken expToken, String extra) { String msg = String.format("Unexpected token (%s), expected %s", - p.getCurrentToken(), expToken); + p.currentToken(), expToken); msg = _colonConcat(msg, extra); return MismatchedInputException.from(p, targetType, msg); } @@ -1654,7 +1789,7 @@ trailingToken, ClassUtil.nameOf(targetType) JsonToken expToken, String extra) { String msg = String.format("Unexpected token (%s), expected %s", - p.getCurrentToken(), expToken); + p.currentToken(), expToken); msg = _colonConcat(msg, extra); return MismatchedInputException.from(p, targetType, msg); } @@ -1890,7 +2025,7 @@ trailingToken, ClassUtil.nameOf(targetType) */ @Deprecated public JsonMappingException mappingException(Class<?> targetClass) { - return mappingException(targetClass, _parser.getCurrentToken()); + return mappingException(targetClass, _parser.currentToken()); } /** @@ -1909,8 +2044,12 @@ trailingToken, ClassUtil.nameOf(targetType) /********************************************************** */ - protected DateFormat getDateFormat() - { + @Deprecated // since 2.12, remove from 2.13 or later + protected DateFormat getDateFormat() { + return _getDateFormat(); + } + + protected DateFormat _getDateFormat() { if (_dateFormat != null) { return _dateFormat; } @@ -1923,4 +2062,48 @@ trailingToken, ClassUtil.nameOf(targetType) _dateFormat = df = (DateFormat) df.clone(); return df; } + + // @since 2.12 + /** + * Helper method for constructing description like "Object value" given + * {@link JsonToken} encountered. + */ + protected String _shapeForToken(JsonToken t) { + if (t != null) { + switch (t) { + // Likely Object values + case START_OBJECT: + case END_OBJECT: + case FIELD_NAME: + return "Object value"; + + // Likely Array values + case START_ARRAY: + case END_ARRAY: + return "Array value"; + + case VALUE_FALSE: + case VALUE_TRUE: + return "Boolean value"; + + case VALUE_EMBEDDED_OBJECT: + return "Embedded Object"; + + case VALUE_NUMBER_FLOAT: + return "Floating-point value"; + case VALUE_NUMBER_INT: + return "Integer value"; + case VALUE_STRING: + return "String value"; + + case VALUE_NULL: + return "Null value"; + + case NOT_AVAILABLE: + default: + return "[Unavailable value]"; + } + } + return "<end of input>"; + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/JsonDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/JsonDeserializer.java index 9cbed3eea..23ac69194 100644 --- a/src/main/java/com/fasterxml/jackson/databind/JsonDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/JsonDeserializer.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.core.*; 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; import com.fasterxml.jackson.databind.util.NameTransformer; @@ -207,7 +208,7 @@ public abstract class JsonDeserializer<T> */ /** - * Method for accessing type of values this deserializer produces. + * Method for accessing concrete physical type of values this deserializer produces. * Note that this information is not guaranteed to be exact -- it * may be a more generic (super-type) -- but it should not be * incorrect (return a non-related type). @@ -215,12 +216,27 @@ public abstract class JsonDeserializer<T> * Default implementation will return null, which means almost same * same as returning <code>Object.class</code> would; that is, that * nothing is known about handled type. - *<p> + * + * @return Physical type of values this deserializer produces, if known; + * {@code null} if not + * * @since 2.3 */ public Class<?> handledType() { return null; } /** + * Method for accessing logical type of values this deserializer produces. + * Typically used for further configuring handling of values, for example, + * to find which coercions are legal. + * + * @return Logical type of values this deserializer produces, if known; + * {@code null} if not + * + * @since 2.12 + */ + public LogicalType logicalType() { return null; } + + /** * Method called to see if deserializer instance is cachable and * usable for other properties of same type (type for which instance * was created). diff --git a/src/main/java/com/fasterxml/jackson/databind/JsonNode.java b/src/main/java/com/fasterxml/jackson/databind/JsonNode.java index 646d54737..0c94ca294 100644 --- a/src/main/java/com/fasterxml/jackson/databind/JsonNode.java +++ b/src/main/java/com/fasterxml/jackson/databind/JsonNode.java @@ -598,7 +598,7 @@ public abstract class JsonNode * and 1 (true), and Strings are parsed using default Java language integer * parsing rules. *<p> - * If representation cannot be converted to an long (including structured types + * If representation cannot be converted to a long (including structured types * like Objects and Arrays), * default value of <b>0</b> will be returned; no exceptions are thrown. */ @@ -612,7 +612,7 @@ public abstract class JsonNode * and 1 (true), and Strings are parsed using default Java language integer * parsing rules. *<p> - * If representation cannot be converted to an long (including structured types + * If representation cannot be converted to a long (including structured types * like Objects and Arrays), * specified <b>defaultValue</b> will be returned; no exceptions are thrown. */ diff --git a/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java b/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java index c669a1b0e..e166f5f3c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java +++ b/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java @@ -213,6 +213,19 @@ public enum MapperFeature implements ConfigFeature */ INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES(true), + /** + * Feature that determines whether nominal property type of {@link Void} is + * allowed for Getter methods to indicate {@code null} valued pseudo-property + * or not. If enabled, such properties are recognized (see [databind#2675] for + * reasons -- mostly things related to frameworks, code generation); if disabled, + * such property accessors (or at least getters) are ignored. + *<p> + * Feature is disabled by default (in 2.12) for backwards compatibility. + * + * @since 2.12 + */ + ALLOW_VOID_VALUED_PROPERTIES(false), + /* /****************************************************** /* Access modifier handling @@ -300,6 +313,21 @@ public enum MapperFeature implements ConfigFeature */ USE_BASE_TYPE_AS_DEFAULT_IMPL(false), + /** + * Feature that enables inferring builder type bindings from the value type + * being deserialized. This requires that the generic type declaration on + * the value type match that on the builder exactly: mismatched type declarations + * are not necessarily detected by databind. + *<p> + * Feature is enabled by default which means that deserialization does + * support deserializing types via builders with type parameters (generic types). + *<p> + * See: https://github.com/FasterXML/jackson-databind/issues/921 + * + * @since 2.12 + */ + INFER_BUILDER_TYPE_BINDINGS(true), + /* /****************************************************** /* View-related features diff --git a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java index 45b6373f7..df1c6ebab 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java +++ b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java @@ -191,8 +191,14 @@ public class ObjectMapper * all non-final types, with exception of small number of * "natural" types (String, Boolean, Integer, Double) that * can be correctly inferred from JSON, and primitives (which - * can not be polymorphic either). Typing is also enabled for - * all array types. + * can not be polymorphic either). + * Typing is also enabled for all array types. + *<p> + * WARNING: most of the time this is <b>NOT</b> the setting you want + * as it tends to add Type Ids everywhere, even in cases + * where type can not be anything other than declared (for example + * if declared value type of a property is {@code final} -- for example, + * properties of type {@code long} (or wrapper {@code Long}). *<p> * Note that the only known use case for this setting is for serialization * when passing instances of final class, and base type is not @@ -401,6 +407,14 @@ public class ObjectMapper */ protected final ConfigOverrides _configOverrides; + /** + * Current set of coercion configuration definitions that define allowed + * (and not allowed) coercions from secondary shapes. + * + * @since 2.12 + */ + protected final CoercionConfigs _coercionConfigs; + /* /********************************************************** /* Configuration settings: mix-in annotations @@ -561,13 +575,15 @@ public class ObjectMapper _typeFactory = src._typeFactory; _injectableValues = src._injectableValues; _configOverrides = src._configOverrides.copy(); + _coercionConfigs = src._coercionConfigs.copy(); _mixIns = src._mixIns.copy(); RootNameLookup rootNames = new RootNameLookup(); _serializationConfig = new SerializationConfig(src._serializationConfig, _subtypeResolver, _mixIns, rootNames, _configOverrides); _deserializationConfig = new DeserializationConfig(src._deserializationConfig, - _subtypeResolver, _mixIns, rootNames, _configOverrides); + _subtypeResolver, _mixIns, rootNames, _configOverrides, + _coercionConfigs); _serializerProvider = src._serializerProvider.copy(); _deserializationContext = src._deserializationContext.copy(); @@ -598,12 +614,8 @@ public class ObjectMapper public ObjectMapper(JsonFactory jf, DefaultSerializerProvider sp, DefaultDeserializationContext dc) { - /* 02-Mar-2009, tatu: Important: we MUST default to using - * the mapping factory, otherwise tree serialization will - * have problems with POJONodes. - * 03-Jan-2010, tatu: and obviously we also must pass 'this', - * to create actual linking. - */ + // 02-Mar-2009, tatu: Important: we MUST default to using the mapping factory, + // otherwise tree serialization will have problems with POJONodes. if (jf == null) { _jsonFactory = new MappingJsonFactory(this); } else { @@ -621,10 +633,12 @@ public class ObjectMapper _mixIns = mixins; BaseSettings base = DEFAULT_BASE.withClassIntrospector(defaultClassIntrospector()); _configOverrides = new ConfigOverrides(); + _coercionConfigs = new CoercionConfigs(); _serializationConfig = new SerializationConfig(base, _subtypeResolver, mixins, rootNames, _configOverrides); _deserializationConfig = new DeserializationConfig(base, - _subtypeResolver, mixins, rootNames, _configOverrides); + _subtypeResolver, mixins, rootNames, _configOverrides, + _coercionConfigs); // Some overrides we may need final boolean needOrder = _jsonFactory.requiresPropertyOrdering(); @@ -736,7 +750,7 @@ public class ObjectMapper protected ObjectWriter _newWriter(SerializationConfig config, FormatSchema schema) { return new ObjectWriter(this, config, schema); } - + /** * Factory method sub-classes must override, to produce {@link ObjectWriter} * instances of proper sub-type @@ -753,7 +767,7 @@ public class ObjectMapper /* Versioned impl /********************************************************** */ - + /** * Method that will return version information stored in and read from jar * that contains this class. @@ -1193,9 +1207,7 @@ public class ObjectMapper */ public JsonParser createParser(File src) throws IOException { _assertNotNull("src", src); - JsonParser p = _jsonFactory.createParser(src); - _deserializationConfig.initialize(p); - return p; + return _deserializationConfig.initialize(_jsonFactory.createParser(src)); } /** @@ -1208,9 +1220,7 @@ public class ObjectMapper */ public JsonParser createParser(URL src) throws IOException { _assertNotNull("src", src); - JsonParser p = _jsonFactory.createParser(src); - _deserializationConfig.initialize(p); - return p; + return _deserializationConfig.initialize(_jsonFactory.createParser(src)); } /** @@ -1223,9 +1233,7 @@ public class ObjectMapper */ public JsonParser createParser(InputStream in) throws IOException { _assertNotNull("in", in); - JsonParser p = _jsonFactory.createParser(in); - _deserializationConfig.initialize(p); - return p; + return _deserializationConfig.initialize(_jsonFactory.createParser(in)); } /** @@ -1238,9 +1246,7 @@ public class ObjectMapper */ public JsonParser createParser(Reader r) throws IOException { _assertNotNull("r", r); - JsonParser p = _jsonFactory.createParser(r); - _deserializationConfig.initialize(p); - return p; + return _deserializationConfig.initialize(_jsonFactory.createParser(r)); } /** @@ -1253,9 +1259,7 @@ public class ObjectMapper */ public JsonParser createParser(byte[] content) throws IOException { _assertNotNull("content", content); - JsonParser p = _jsonFactory.createParser(content); - _deserializationConfig.initialize(p); - return p; + return _deserializationConfig.initialize(_jsonFactory.createParser(content)); } /** @@ -1268,9 +1272,7 @@ public class ObjectMapper */ public JsonParser createParser(byte[] content, int offset, int len) throws IOException { _assertNotNull("content", content); - JsonParser p = _jsonFactory.createParser(content, offset, len); - _deserializationConfig.initialize(p); - return p; + return _deserializationConfig.initialize(_jsonFactory.createParser(content, offset, len)); } /** @@ -1283,9 +1285,7 @@ public class ObjectMapper */ public JsonParser createParser(String content) throws IOException { _assertNotNull("content", content); - JsonParser p = _jsonFactory.createParser(content); - _deserializationConfig.initialize(p); - return p; + return _deserializationConfig.initialize(_jsonFactory.createParser(content)); } /** @@ -1298,9 +1298,7 @@ public class ObjectMapper */ public JsonParser createParser(char[] content) throws IOException { _assertNotNull("content", content); - JsonParser p = _jsonFactory.createParser(content); - _deserializationConfig.initialize(p); - return p; + return _deserializationConfig.initialize(_jsonFactory.createParser(content)); } /** @@ -1313,9 +1311,7 @@ public class ObjectMapper */ public JsonParser createParser(char[] content, int offset, int len) throws IOException { _assertNotNull("content", content); - JsonParser p = _jsonFactory.createParser(content, offset, len); - _deserializationConfig.initialize(p); - return p; + return _deserializationConfig.initialize(_jsonFactory.createParser(content, offset, len)); } /** @@ -1328,9 +1324,7 @@ public class ObjectMapper */ public JsonParser createParser(DataInput content) throws IOException { _assertNotNull("content", content); - JsonParser p = _jsonFactory.createParser(content); - _deserializationConfig.initialize(p); - return p; + return _deserializationConfig.initialize(_jsonFactory.createParser(content)); } /** @@ -1342,9 +1336,7 @@ public class ObjectMapper * @since 2.11 */ public JsonParser createNonBlockingByteArrayParser() throws IOException { - JsonParser p = _jsonFactory.createNonBlockingByteArrayParser(); - _deserializationConfig.initialize(p); - return p; + return _deserializationConfig.initialize(_jsonFactory.createNonBlockingByteArrayParser()); } /* @@ -2052,7 +2044,7 @@ public class ObjectMapper /* /********************************************************** - /* Configuration, basic type handling + /* Configuration, config, coercion overrides /********************************************************** */ @@ -2079,6 +2071,45 @@ public class ObjectMapper /* /********************************************************** + /* Configuration, coercion config (2.x only) + /********************************************************** + */ + + /** + * Accessor for {@link MutableCoercionConfig} through which + * default (fallback) coercion configurations can be changed. + * Note that such settings are only applied if more specific + * (by logical and physical type) configuration have + * not been defined. + * + * @since 2.12 + */ + public MutableCoercionConfig coercionConfigDefaults() { + return _coercionConfigs.defaultCoercions(); + } + + /** + * Accessor for {@link MutableCoercionConfig} through which + * coercion configuration for specified logical target type can be set. + * + * @since 2.12 + */ + public MutableCoercionConfig coercionConfigFor(LogicalType logicalType) { + return _coercionConfigs.findOrCreateCoercion(logicalType); + } + + /** + * Accessor for {@link MutableCoercionConfig} through which + * coercion configuration for specified physical target type can be set. + * + * @since 2.12 + */ + public MutableCoercionConfig coercionConfigFor(Class<?> physicalType) { + return _coercionConfigs.findOrCreateCoercion(physicalType); + } + + /* + /********************************************************** /* Configuration, basic type handling /********************************************************** */ @@ -2827,7 +2858,7 @@ public class ObjectMapper _assertNotNull("p", p); // Must check for EOF here before calling readValue(), since that'll choke on it otherwise DeserializationConfig cfg = getDeserializationConfig(); - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == null) { t = p.nextToken(); if (t == null) { @@ -3303,7 +3334,7 @@ public class ObjectMapper return createDeserializationContext(null, getDeserializationConfig()).hasValueDeserializerFor(type, cause); } - + /* /********************************************************** /* Extended Public API, deserialization, @@ -4474,26 +4505,19 @@ public class ObjectMapper JavaType valueType) throws IOException { - /* First: may need to read the next token, to initialize - * state (either before first read from parser, or after - * previous token has been cleared) - */ - Object result; + // First: may need to read the next token, to initialize + // state (either before first read from parser, or after + // previous token has been cleared) + final Object result; JsonToken t = _initForReading(p, valueType); - final DeserializationContext ctxt = createDeserializationContext(p, cfg); + final DefaultDeserializationContext ctxt = createDeserializationContext(p, cfg); if (t == JsonToken.VALUE_NULL) { // Ask JsonDeserializer what 'null value' to use: result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt); } else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) { result = null; } else { // pointing to event other than null - JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, valueType); - // ok, let's get the value - if (cfg.useRootWrapping()) { - result = _unwrapAndDeserialize(p, ctxt, cfg, valueType, deser); - } else { - result = deser.deserialize(p, ctxt); - } + result = ctxt.readRootValue(p, valueType, _findRootDeserializer(ctxt, valueType), null); } // Need to consume the token too p.clearCurrentToken(); @@ -4507,22 +4531,18 @@ public class ObjectMapper throws IOException { try (JsonParser p = p0) { - Object result; - JsonToken t = _initForReading(p, valueType); + final Object result; final DeserializationConfig cfg = getDeserializationConfig(); - final DeserializationContext ctxt = createDeserializationContext(p, cfg); + final DefaultDeserializationContext ctxt = createDeserializationContext(p, cfg); + JsonToken t = _initForReading(p, valueType); if (t == JsonToken.VALUE_NULL) { // Ask JsonDeserializer what 'null value' to use: result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt); } else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) { result = null; } else { - JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, valueType); - if (cfg.useRootWrapping()) { - result = _unwrapAndDeserialize(p, ctxt, cfg, valueType, deser); - } else { - result = deser.deserialize(p, ctxt); - } + result = ctxt.readRootValue(p, valueType, + _findRootDeserializer(ctxt, valueType), null); ctxt.checkUnresolvedObjectId(); } if (cfg.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) { @@ -4548,7 +4568,7 @@ public class ObjectMapper // special requirements by tree reading (no fail on eof) cfg.initialize(p); // since 2.5 - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == null) { t = p.nextToken(); if (t == null) { @@ -4557,72 +4577,25 @@ public class ObjectMapper return cfg.getNodeFactory().missingNode(); } } - final boolean checkTrailing = cfg.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); - DeserializationContext ctxt; - JsonNode resultNode; + final JsonNode resultNode; + final DefaultDeserializationContext ctxt = createDeserializationContext(p, cfg); if (t == JsonToken.VALUE_NULL) { resultNode = cfg.getNodeFactory().nullNode(); - if (!checkTrailing) { - return resultNode; - } - ctxt = createDeserializationContext(p, cfg); } else { - ctxt = createDeserializationContext(p, cfg); - JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, valueType); - if (cfg.useRootWrapping()) { - resultNode = (JsonNode) _unwrapAndDeserialize(p, ctxt, cfg, valueType, deser); - } else { - resultNode = (JsonNode) deser.deserialize(p, ctxt); - } + resultNode = (JsonNode) ctxt.readRootValue(p, valueType, + _findRootDeserializer(ctxt, valueType), null); + // No ObjectIds so can ignore +// ctxt.checkUnresolvedObjectId(); } - if (checkTrailing) { + if (cfg.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) { _verifyNoTrailingTokens(p, ctxt, valueType); } - // No ObjectIds so can ignore -// ctxt.checkUnresolvedObjectId(); return resultNode; } } - protected Object _unwrapAndDeserialize(JsonParser p, DeserializationContext ctxt, - DeserializationConfig config, - JavaType rootType, JsonDeserializer<Object> deser) - throws IOException - { - PropertyName expRootName = config.findRootName(rootType); - // 12-Jun-2015, tatu: Should try to support namespaces etc but... - String expSimpleName = expRootName.getSimpleName(); - if (p.getCurrentToken() != JsonToken.START_OBJECT) { - ctxt.reportWrongTokenException(rootType, JsonToken.START_OBJECT, - "Current token not START_OBJECT (needed to unwrap root name '%s'), but %s", - expSimpleName, p.getCurrentToken()); - } - if (p.nextToken() != JsonToken.FIELD_NAME) { - ctxt.reportWrongTokenException(rootType, JsonToken.FIELD_NAME, - "Current token not FIELD_NAME (to contain expected root name '%s'), but %s", - expSimpleName, p.getCurrentToken()); - } - String actualName = p.getCurrentName(); - if (!expSimpleName.equals(actualName)) { - ctxt.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(); - Object result = deser.deserialize(p, ctxt); - // and last, verify that we now get matching END_OBJECT - if (p.nextToken() != JsonToken.END_OBJECT) { - ctxt.reportWrongTokenException(rootType, JsonToken.END_OBJECT, - "Current token not END_OBJECT (to match wrapper object with root name '%s'), but %s", - expSimpleName, p.getCurrentToken()); - } - if (config.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) { - _verifyNoTrailingTokens(p, ctxt, rootType); - } - return result; - } + /** * Internal helper method called to create an instance of {@link DeserializationContext} @@ -4656,7 +4629,7 @@ public class ObjectMapper // First: must point to a token; if not pointing to one, advance. // This occurs before first read from JsonParser, as well as // after clearing of current token. - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == null) { // and then we must get something... t = p.nextToken(); diff --git a/src/main/java/com/fasterxml/jackson/databind/ObjectReader.java b/src/main/java/com/fasterxml/jackson/databind/ObjectReader.java index 483e55338..6c817c186 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ObjectReader.java +++ b/src/main/java/com/fasterxml/jackson/databind/ObjectReader.java @@ -343,16 +343,13 @@ public class ObjectReader protected JsonToken _initForReading(DeserializationContext ctxt, JsonParser p) throws IOException { - if (_schema != null) { - p.setSchema(_schema); - } - _config.initialize(p); // since 2.5 + _config.initialize(p, _schema); /* First: must point to a token; if not pointing to one, advance. * This occurs before first read from JsonParser, as well as * after clearing of current token. */ - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == null) { // and then we must get something... t = p.nextToken(); if (t == null) { @@ -376,10 +373,7 @@ public class ObjectReader protected void _initForMultiRead(DeserializationContext ctxt, JsonParser p) throws IOException { - if (_schema != null) { - p.setSchema(_schema); - } - _config.initialize(p); + _config.initialize(p, _schema); } /* @@ -1024,9 +1018,7 @@ public class ObjectReader */ public JsonParser createParser(File src) throws IOException { _assertNotNull("src", src); - JsonParser p = _parserFactory.createParser(src); - _config.initialize(p); - return p; + return _config.initialize(_parserFactory.createParser(src), _schema); } /** @@ -1039,9 +1031,7 @@ public class ObjectReader */ public JsonParser createParser(URL src) throws IOException { _assertNotNull("src", src); - JsonParser p = _parserFactory.createParser(src); - _config.initialize(p); - return p; + return _config.initialize(_parserFactory.createParser(src), _schema); } /** @@ -1054,9 +1044,7 @@ public class ObjectReader */ public JsonParser createParser(InputStream in) throws IOException { _assertNotNull("in", in); - JsonParser p = _parserFactory.createParser(in); - _config.initialize(p); - return p; + return _config.initialize(_parserFactory.createParser(in), _schema); } /** @@ -1069,9 +1057,7 @@ public class ObjectReader */ public JsonParser createParser(Reader r) throws IOException { _assertNotNull("r", r); - JsonParser p = _parserFactory.createParser(r); - _config.initialize(p); - return p; + return _config.initialize(_parserFactory.createParser(r), _schema); } /** @@ -1084,9 +1070,7 @@ public class ObjectReader */ public JsonParser createParser(byte[] content) throws IOException { _assertNotNull("content", content); - JsonParser p = _parserFactory.createParser(content); - _config.initialize(p); - return p; + return _config.initialize(_parserFactory.createParser(content), _schema); } /** @@ -1099,9 +1083,7 @@ public class ObjectReader */ public JsonParser createParser(byte[] content, int offset, int len) throws IOException { _assertNotNull("content", content); - JsonParser p = _parserFactory.createParser(content, offset, len); - _config.initialize(p); - return p; + return _config.initialize(_parserFactory.createParser(content, offset, len), _schema); } /** @@ -1114,9 +1096,7 @@ public class ObjectReader */ public JsonParser createParser(String content) throws IOException { _assertNotNull("content", content); - JsonParser p = _parserFactory.createParser(content); - _config.initialize(p); - return p; + return _config.initialize(_parserFactory.createParser(content), _schema); } /** @@ -1129,9 +1109,7 @@ public class ObjectReader */ public JsonParser createParser(char[] content) throws IOException { _assertNotNull("content", content); - JsonParser p = _parserFactory.createParser(content); - _config.initialize(p); - return p; + return _config.initialize(_parserFactory.createParser(content), _schema); } /** @@ -1144,9 +1122,7 @@ public class ObjectReader */ public JsonParser createParser(char[] content, int offset, int len) throws IOException { _assertNotNull("content", content); - JsonParser p = _parserFactory.createParser(content, offset, len); - _config.initialize(p); - return p; + return _config.initialize(_parserFactory.createParser(content, offset, len), _schema); } /** @@ -1159,9 +1135,7 @@ public class ObjectReader */ public JsonParser createParser(DataInput content) throws IOException { _assertNotNull("content", content); - JsonParser p = _parserFactory.createParser(content); - _config.initialize(p); - return p; + return _config.initialize(_parserFactory.createParser(content), _schema); } /** @@ -1173,9 +1147,7 @@ public class ObjectReader * @since 2.11 */ public JsonParser createNonBlockingByteArrayParser() throws IOException { - JsonParser p = _parserFactory.createNonBlockingByteArrayParser(); - _config.initialize(p); - return p; + return _config.initialize(_parserFactory.createNonBlockingByteArrayParser(), _schema); } /* @@ -2019,11 +1991,10 @@ public class ObjectReader */ protected Object _bind(JsonParser p, Object valueToUpdate) throws IOException { - /* First: may need to read the next token, to initialize state (either - * before first read from parser, or after previous token has been cleared) - */ + // First: may need to read the next token, to initialize state (either + // before first read from parser, or after previous token has been cleared) Object result; - final DeserializationContext ctxt = createDeserializationContext(p); + final DefaultDeserializationContext ctxt = createDeserializationContext(p); JsonToken t = _initForReading(ctxt, p); if (t == JsonToken.VALUE_NULL) { if (valueToUpdate == null) { @@ -2034,18 +2005,7 @@ public class ObjectReader } else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) { result = valueToUpdate; } else { // pointing to event other than null - JsonDeserializer<Object> deser = _findRootDeserializer(ctxt); - if (_unwrapRoot) { - result = _unwrapAndDeserialize(p, ctxt, _valueType, deser); - } else { - if (valueToUpdate == null) { - result = deser.deserialize(p, ctxt); - } else { - // 20-Mar-2017, tatu: Important! May be different from `valueToUpdate` - // for immutable Objects like Java arrays; logical result - result = deser.deserialize(p, ctxt, valueToUpdate); - } - } + result = ctxt.readRootValue(p, _valueType, _findRootDeserializer(ctxt), _valueToUpdate); } // Need to consume the token too p.clearCurrentToken(); @@ -2060,7 +2020,7 @@ public class ObjectReader try (JsonParser p = p0) { Object result; - DeserializationContext ctxt = createDeserializationContext(p); + final DefaultDeserializationContext ctxt = createDeserializationContext(p); JsonToken t = _initForReading(ctxt, p); if (t == JsonToken.VALUE_NULL) { if (_valueToUpdate == null) { @@ -2071,17 +2031,7 @@ public class ObjectReader } else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) { result = _valueToUpdate; } else { - JsonDeserializer<Object> deser = _findRootDeserializer(ctxt); - if (_unwrapRoot) { - result = _unwrapAndDeserialize(p, ctxt, _valueType, deser); - } else { - if (_valueToUpdate == null) { - result = deser.deserialize(p, ctxt); - } else { - deser.deserialize(p, ctxt, _valueToUpdate); - result = _valueToUpdate; - } - } + result = ctxt.readRootValue(p, _valueType, _findRootDeserializer(ctxt), _valueToUpdate); } if (_config.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) { _verifyNoTrailingTokens(p, ctxt, _valueType); @@ -2104,31 +2054,21 @@ public class ObjectReader p.setSchema(_schema); } - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == null) { t = p.nextToken(); if (t == null) { return _config.getNodeFactory().missingNode(); } } - final DeserializationContext ctxt; + final DefaultDeserializationContext ctxt = createDeserializationContext(p); final JsonNode resultNode; - final boolean checkTrailing = _config.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); if (t == JsonToken.VALUE_NULL) { resultNode = _config.getNodeFactory().nullNode(); - if (!checkTrailing) { - return resultNode; - } - ctxt = createDeserializationContext(p); } else { - ctxt = createDeserializationContext(p); - final JsonDeserializer<Object> deser = _findTreeDeserializer(ctxt); - if (_unwrapRoot) { - resultNode = (JsonNode) _unwrapAndDeserialize(p, ctxt, _jsonNodeType(), deser); - } else { - resultNode = (JsonNode) deser.deserialize(p, ctxt); - } + // Will not be called for merge (need not pass _valueToUpdate) + resultNode = (JsonNode) ctxt.readRootValue(p, _jsonNodeType(), _findTreeDeserializer(ctxt), null); } if (_config.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) { _verifyNoTrailingTokens(p, ctxt, _jsonNodeType()); @@ -2146,37 +2086,27 @@ public class ObjectReader if (_schema != null) { p.setSchema(_schema); } - JsonToken t = p.getCurrentToken(); + JsonToken t = p.currentToken(); if (t == null) { t = p.nextToken(); if (t == null) { return null; } } - final DeserializationContext ctxt; + final DefaultDeserializationContext ctxt = createDeserializationContext(p); final JsonNode resultNode; - final boolean checkTrailing = _config.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); if (t == JsonToken.VALUE_NULL) { resultNode = _config.getNodeFactory().nullNode(); - if (!checkTrailing) { - return resultNode; - } - ctxt = createDeserializationContext(p); } else { - ctxt = createDeserializationContext(p); - final JsonDeserializer<Object> deser = _findTreeDeserializer(ctxt); - if (_unwrapRoot) { - resultNode = (JsonNode) _unwrapAndDeserialize(p, ctxt, _jsonNodeType(), deser); - } else { - resultNode = (JsonNode) deser.deserialize(p, ctxt); - } + // Will not be called for merge (need not pass _valueToUpdate) + resultNode = (JsonNode) ctxt.readRootValue(p, _jsonNodeType(), _findTreeDeserializer(ctxt), null); } - if (checkTrailing) { + if (_config.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) { _verifyNoTrailingTokens(p, ctxt, _jsonNodeType()); } return resultNode; } - + /** * @since 2.1 */ @@ -2188,50 +2118,6 @@ public class ObjectReader return _newIterator(p, ctxt, _findRootDeserializer(ctxt), true); } - protected Object _unwrapAndDeserialize(JsonParser p, DeserializationContext ctxt, - JavaType rootType, JsonDeserializer<Object> deser) throws IOException - { - PropertyName expRootName = _config.findRootName(rootType); - // 12-Jun-2015, tatu: Should try to support namespaces etc but... - String expSimpleName = expRootName.getSimpleName(); - - if (p.getCurrentToken() != JsonToken.START_OBJECT) { - ctxt.reportWrongTokenException(rootType, JsonToken.START_OBJECT, - "Current token not START_OBJECT (needed to unwrap root name '%s'), but %s", - expSimpleName, p.getCurrentToken()); - } - if (p.nextToken() != JsonToken.FIELD_NAME) { - ctxt.reportWrongTokenException(rootType, JsonToken.FIELD_NAME, - "Current token not FIELD_NAME (to contain expected root name '%s'), but %s", - expSimpleName, p.getCurrentToken()); - } - String actualName = p.getCurrentName(); - if (!expSimpleName.equals(actualName)) { - ctxt.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(); - Object result; - if (_valueToUpdate == null) { - result = deser.deserialize(p, ctxt); - } else { - deser.deserialize(p, ctxt, _valueToUpdate); - result = _valueToUpdate; - } - // and last, verify that we now get matching END_OBJECT - if (p.nextToken() != JsonToken.END_OBJECT) { - ctxt.reportWrongTokenException(rootType, JsonToken.END_OBJECT, - "Current token not END_OBJECT (to match wrapper object with root name '%s'), but %s", - expSimpleName, p.getCurrentToken()); - } - if (_config.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) { - _verifyNoTrailingTokens(p, ctxt, _valueType); - } - return result; - } - /** * Consider filter when creating JsonParser. */ @@ -2364,6 +2250,11 @@ public class ObjectReader return _context.createInstance(_config, p, _injectableValues); } + // @since 2.12 -- needed for Deserializer pre-fetch + protected DefaultDeserializationContext createDummyDeserializationContext() { + return _context.createDummyInstance(_config); + } + protected InputStream _inputStream(URL src) throws IOException { return src.openStream(); } @@ -2450,7 +2341,7 @@ public class ObjectReader if (deser == null) { try { // If not, need to resolve; for which we need a temporary context as well: - DeserializationContext ctxt = createDeserializationContext(null); + DeserializationContext ctxt = createDummyDeserializationContext(); deser = ctxt.findRootValueDeserializer(valueType); if (deser != null) { _rootDeserializers.put(valueType, deser); diff --git a/src/main/java/com/fasterxml/jackson/databind/PropertyNamingStrategy.java b/src/main/java/com/fasterxml/jackson/databind/PropertyNamingStrategy.java index 0b5563be1..f0bc87e59 100644 --- a/src/main/java/com/fasterxml/jackson/databind/PropertyNamingStrategy.java +++ b/src/main/java/com/fasterxml/jackson/databind/PropertyNamingStrategy.java @@ -299,7 +299,7 @@ public class PropertyNamingStrategy // NOTE: was abstract until 2.7 * <li>"user__name" is translated to "user__name" * (unchanged, with two underscores)</li></ul> * - * @since 2.7 (was previously called } + * @since 2.7 (was previously called {@link LowerCaseWithUnderscoresStrategy}) */ public static class SnakeCaseStrategy extends PropertyNamingStrategyBase { diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java index a1b934083..f0a25ea78 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java @@ -15,13 +15,13 @@ import com.fasterxml.jackson.databind.util.StdDateFormat; /** * Immutable container class used to store simple configuration - * settings. Since instances are fully immutable, instances can + * settings for both serialization and deserialization. + * Since instances are fully immutable, instances can * be freely shared and used without synchronization. */ public final class BaseSettings implements java.io.Serializable { - // for 2.6 private static final long serialVersionUID = 1L; /** @@ -91,7 +91,7 @@ public final class BaseSettings */ /** - * Custom date format to use for de-serialization. If specified, will be + * Custom date format to use for deserialization. If specified, will be * used instead of {@link com.fasterxml.jackson.databind.util.StdDateFormat}. *<p> * Note that the configured format object will be cloned once per diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionAction.java b/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionAction.java new file mode 100644 index 000000000..c535ed256 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionAction.java @@ -0,0 +1,42 @@ +package com.fasterxml.jackson.databind.cfg; + +import com.fasterxml.jackson.databind.type.LogicalType; + +/** + * Set of possible actions for requested coercion from an + * input shape {@link CoercionInputShape} + * that does not directly or naturally match target type + * ({@link LogicalType}). + * This action is suggestion for deserializers to use in cases + * where alternate actions could be appropriate: it is up to deserializer + * to check configured action and take it into consideration. + * + * @since 2.12 + */ +public enum CoercionAction +{ + /** + * Action to fail coercion attempt with exceptipn + */ + Fail, + + /** + * Action to attempt coercion (which may lead to failure) + */ + TryConvert, + + /** + * Action to convert to {@code null} value + */ + AsNull, + + /** + * Action to convert to "empty" value for type, whatever that is: for + * primitive types and their wrappers this is "default" value (for example, + * for {@code int} that would be {@code 0}); for {@link java.util.Collection}s + * empty collection; for POJOs instance configured with default constructor + * and so on. + */ + AsEmpty; + ; +} diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionConfig.java b/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionConfig.java new file mode 100644 index 000000000..3bfa17b79 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionConfig.java @@ -0,0 +1,41 @@ +package com.fasterxml.jackson.databind.cfg; + +import java.util.Arrays; + +/** + * @since 2.12 + */ +public class CoercionConfig + implements java.io.Serializable +{ + private static final long serialVersionUID = 1L; + + private final static int INPUT_SHAPE_COUNT = CoercionInputShape.values().length; + + protected Boolean _acceptBlankAsEmpty; + + /** + * Mapping from {@link CoercionInputShape} into corresponding + * {@link CoercionAction}. + */ + protected final CoercionAction[] _coercionsByShape; + + public CoercionConfig() { + _coercionsByShape = new CoercionAction[INPUT_SHAPE_COUNT]; + _acceptBlankAsEmpty = false; + } + + protected CoercionConfig(CoercionConfig src) { + _acceptBlankAsEmpty = src._acceptBlankAsEmpty; + _coercionsByShape = Arrays.copyOf(src._coercionsByShape, + src._coercionsByShape.length); + } + + public CoercionAction findAction(CoercionInputShape shape) { + return _coercionsByShape[shape.ordinal()]; + } + + public Boolean getAcceptBlankAsEmpty() { + return _acceptBlankAsEmpty; + } +} diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionConfigs.java b/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionConfigs.java new file mode 100644 index 000000000..50fae96dc --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionConfigs.java @@ -0,0 +1,307 @@ +package com.fasterxml.jackson.databind.cfg; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.type.LogicalType; + +/** + * @since 2.12 + */ +public class CoercionConfigs + implements java.io.Serializable +{ + private static final long serialVersionUID = 1L; + + private final static int TARGET_TYPE_COUNT = LogicalType.values().length; + + /** + * Global default for cases not explicitly covered + */ + protected CoercionAction _defaultAction; + + /** + * Default coercion definitions used if no overrides found + * by logical or physical type. + */ + protected final MutableCoercionConfig _defaultCoercions; + + /** + * Coercion definitions by logical type ({@link LogicalType}) + */ + protected MutableCoercionConfig[] _perTypeCoercions; + + /** + * Coercion definitions by physical type (Class). + */ + protected Map<Class<?>, MutableCoercionConfig> _perClassCoercions; + + /* + /********************************************************************** + /* Life cycle + /********************************************************************** + */ + + public CoercionConfigs() { + this(CoercionAction.TryConvert, new MutableCoercionConfig(), + null, null); + } + + protected CoercionConfigs(CoercionAction defaultAction, + MutableCoercionConfig defaultCoercions, + MutableCoercionConfig[] perTypeCoercions, + Map<Class<?>, MutableCoercionConfig> perClassCoercions) + { + _defaultCoercions = defaultCoercions; + _defaultAction = defaultAction; + _perTypeCoercions = perTypeCoercions; + _perClassCoercions = perClassCoercions; + } + + /** + * Method called to create a non-shared copy of configuration settings, + * to be used by another {@link com.fasterxml.jackson.databind.ObjectMapper} + * instance. + * + * @return A non-shared copy of configuration settings + */ + public CoercionConfigs copy() + { + MutableCoercionConfig[] newPerType; + if (_perTypeCoercions == null) { + newPerType = null; + } else { + final int size = _perTypeCoercions.length; + newPerType = new MutableCoercionConfig[size]; + for (int i = 0; i < size; ++i) { + newPerType[i] = _copy(_perTypeCoercions[i]); + } + } + Map<Class<?>, MutableCoercionConfig> newPerClass; + if (_perClassCoercions == null) { + newPerClass = null; + } else { + newPerClass = new HashMap<>(); + for (Map.Entry<Class<?>, MutableCoercionConfig> entry : _perClassCoercions.entrySet()) { + newPerClass.put(entry.getKey(), entry.getValue().copy()); + } + } + return new CoercionConfigs(_defaultAction, _defaultCoercions.copy(), + newPerType, newPerClass); + } + + private static MutableCoercionConfig _copy(MutableCoercionConfig src) { + if (src == null) { + return null; + } + return src.copy(); + } + + /* + /********************************************************************** + /* Mutators: global defaults + /********************************************************************** + */ + + public MutableCoercionConfig defaultCoercions() { + return _defaultCoercions; + } + + /* + /********************************************************************** + /* Mutators: per type + /********************************************************************** + */ + + public MutableCoercionConfig findOrCreateCoercion(LogicalType type) { + if (_perTypeCoercions == null) { + _perTypeCoercions = new MutableCoercionConfig[TARGET_TYPE_COUNT]; + } + MutableCoercionConfig config = _perTypeCoercions[type.ordinal()]; + if (config == null) { + _perTypeCoercions[type.ordinal()] = config = new MutableCoercionConfig(); + } + return config; + } + + public MutableCoercionConfig findOrCreateCoercion(Class<?> type) { + if (_perClassCoercions == null) { + _perClassCoercions = new HashMap<>(); + } + MutableCoercionConfig config = _perClassCoercions.get(type); + if (config == null) { + config = new MutableCoercionConfig(); + _perClassCoercions.put(type, config); + } + return config; + } + + /* + /********************************************************************** + /* Access + /********************************************************************** + */ + + /** + * General-purpose accessor for finding what to do when specified coercion + * from shape that is now always allowed to be coerced from is requested. + * + * @param config Currently active deserialization configuration + * @param targetType Logical target type of coercion + * @param targetClass Physical target type of coercion + * @param inputShape Input shape to coerce from + * + * @return CoercionAction configured for specified coercion + * + * @since 2.12 + */ + public CoercionAction findCoercion(DeserializationConfig config, + LogicalType targetType, + Class<?> targetClass, CoercionInputShape inputShape) + { + // First, see if there is exact match for physical type + if ((_perClassCoercions != null) && (targetClass != null)) { + MutableCoercionConfig cc = _perClassCoercions.get(targetClass); + if (cc != null) { + CoercionAction act = cc.findAction(inputShape); + if (act != null) { + return act; + } + } + } + + // If not, maybe by logical type + if ((_perTypeCoercions != null) && (targetType != null)) { + MutableCoercionConfig cc = _perTypeCoercions[targetType.ordinal()]; + if (cc != null) { + CoercionAction act = cc.findAction(inputShape); + if (act != null) { + return act; + } + } + } + + // Barring that, default coercion for input shape? + CoercionAction act = _defaultCoercions.findAction(inputShape); + if (act != null) { + return act; + } + + // Otherwise there are some legacy features that can provide answer + if (inputShape == CoercionInputShape.EmptyArray) { + // Default for setting is false + return config.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) ? + CoercionAction.AsNull : CoercionAction.Fail; + } + if ((inputShape == CoercionInputShape.Float) + && (targetType == LogicalType.Integer)) { + // Default for setting in 2.x is true + return config.isEnabled(DeserializationFeature.ACCEPT_FLOAT_AS_INT) ? + CoercionAction.TryConvert : CoercionAction.Fail; + } + + // classic scalars are numbers, booleans; but date/time also considered + // scalar for this particular purpose + final boolean baseScalar = (targetType == LogicalType.Float) + || (targetType == LogicalType.Integer) + || (targetType == LogicalType.Boolean) + || (targetType == LogicalType.DateTime); + + if (baseScalar) { + // Default for setting in 2.x is true + if (!config.isEnabled(MapperFeature.ALLOW_COERCION_OF_SCALARS)) { + return CoercionAction.Fail; + } + } + + if (inputShape == CoercionInputShape.EmptyString) { + // Since coercion of scalar must be enabled (see check above), allow empty-string + // coercions by default even without this setting + if (baseScalar + // Default for setting is false + || config.isEnabled(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)) { + return CoercionAction.AsNull; + } + // 09-Jun-2020, tatu: Seems necessary to support backwards-compatibility with + // 2.11, wrt "FromStringDeserializer" supported types + if (targetType == LogicalType.OtherScalar) { + return CoercionAction.TryConvert; + } + // But block from allowing structured types like POJOs, Maps etc + return CoercionAction.Fail; + } + + // and all else failing, return default + return _defaultAction; + } + + /** + * More specialized accessor called in case of input being a blank + * String (one consisting of only white space characters with length of at least one). + * Will basically first determine if "blank as empty" is allowed: if not, + * returns {@code actionIfBlankNotAllowed}, otherwise returns action for + * {@link CoercionInputShape#EmptyString}. + * + * @param config Currently active deserialization configuration + * @param targetType Logical target type of coercion + * @param targetClass Physical target type of coercion + * @param actionIfBlankNotAllowed Return value to use in case "blanks as empty" + * is not allowed + * + * @return CoercionAction configured for specified coercion from blank string + */ + public CoercionAction findCoercionFromBlankString(DeserializationConfig config, + LogicalType targetType, + Class<?> targetClass, + CoercionAction actionIfBlankNotAllowed) + { + Boolean acceptBlankAsEmpty = null; + CoercionAction action = null; + + // First, see if there is exact match for physical type + if ((_perClassCoercions != null) && (targetClass != null)) { + MutableCoercionConfig cc = _perClassCoercions.get(targetClass); + if (cc != null) { + acceptBlankAsEmpty = cc.getAcceptBlankAsEmpty(); + action = cc.findAction(CoercionInputShape.EmptyString); + } + } + + // If not, maybe by logical type + if ((_perTypeCoercions != null) && (targetType != null)) { + MutableCoercionConfig cc = _perTypeCoercions[targetType.ordinal()]; + if (cc != null) { + if (acceptBlankAsEmpty == null) { + acceptBlankAsEmpty = cc.getAcceptBlankAsEmpty(); + } + if (action == null) { + action = cc.findAction(CoercionInputShape.EmptyString); + } + } + } + + // Barring that, default coercion for input shape? + if (acceptBlankAsEmpty == null) { + acceptBlankAsEmpty = _defaultCoercions.getAcceptBlankAsEmpty(); + } + if (action == null) { + action = _defaultCoercions.findAction(CoercionInputShape.EmptyString); + } + + // First: if using blank as empty is no-go, return what caller specified + if (!Boolean.TRUE.equals(acceptBlankAsEmpty)) { + return actionIfBlankNotAllowed; + } + + // Otherwise, if action found, return that + if (action != null) { + return action; + } + // If not, one specific legacy setting to consider... + return config.isEnabled(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) ? + CoercionAction.AsNull : CoercionAction.Fail; + } +} diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionInputShape.java b/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionInputShape.java new file mode 100644 index 000000000..1bd2c2588 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/CoercionInputShape.java @@ -0,0 +1,96 @@ +package com.fasterxml.jackson.databind.cfg; + +import com.fasterxml.jackson.databind.type.LogicalType; + +/** + * Set of input types (which mostly match one of + * {@link com.fasterxml.jackson.core.JsonToken} types) used for + * configuring {@link CoercionAction}s to take when reading + * input into target types (specific type or {@link LogicalType}). + * Contains both physical input shapes (which match one of + * {@link com.fasterxml.jackson.core.JsonToken} types) and a few + * logical input shapes ("empty" variants). + *<p> + * Note that {@code null} input shape is explicitly not included as + * its configuration is distinct from other types. + * + * @since 2.12 + */ +public enum CoercionInputShape +{ + // Physical types + + /** + * Shape of Array values from input (token sequence from + * {@link com.fasterxml.jackson.core.JsonToken#START_ARRAY} to + * {@link com.fasterxml.jackson.core.JsonToken#END_ARRAY}) + */ + Array, + + /** + * Shape of Object values from input (token sequence from + * {@link com.fasterxml.jackson.core.JsonToken#START_OBJECT} to + * {@link com.fasterxml.jackson.core.JsonToken#END_OBJECT}) + */ + Object, + + /** + * Shape of integral (non-floating point) numeric values from input (token + * {@link com.fasterxml.jackson.core.JsonToken#VALUE_NUMBER_INT}) + */ + Integer, + + /** + * Shape of floating point (non-integral) numeric values from input (token + * {@link com.fasterxml.jackson.core.JsonToken#VALUE_NUMBER_FLOAT}) + */ + Float, + + /** + * Shape of boolean values from input (tokens + * {@link com.fasterxml.jackson.core.JsonToken#VALUE_TRUE} and + * {@link com.fasterxml.jackson.core.JsonToken#VALUE_FALSE}) + */ + Boolean, + + /** + * Shape of string values from input (tokens + * {@link com.fasterxml.jackson.core.JsonToken#VALUE_STRING}) + */ + String, + + /** + * Shape of binary data values from input, if expressed natively + * by underlying format (many + * textual formats, including JSON, do not have such shape); if so + * generally seen as {@link com.fasterxml.jackson.core.JsonToken#VALUE_EMBEDDED_OBJECT}. + */ + Binary, + + // Logical types + + /** + * Special case of Array values with no actual content (sequence of 2 tokens: + * {@link com.fasterxml.jackson.core.JsonToken#START_ARRAY}, + * {@link com.fasterxml.jackson.core.JsonToken#END_ARRAY}): + * usually used to allow special coercion into "empty" or {@code null} target type. + */ + EmptyArray, + + /** + * Special case of Object values with no actual content (sequence of 2 tokens: + * {@link com.fasterxml.jackson.core.JsonToken#START_OBJECT}, + * {@link com.fasterxml.jackson.core.JsonToken#END_OBJECT}): + * usually used to allow special coercion into "empty" or {@code null} target type. + */ + EmptyObject, + + /** + * Special case for String values with no content (or, if allowed by format or specific + * configuration, also "blank" String, that is, all-whitespace content). + * usually used to allow special coercion into "empty" or {@code null} target type. + */ + EmptyString + + ; +} diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/ConfigOverrides.java b/src/main/java/com/fasterxml/jackson/databind/cfg/ConfigOverrides.java index dfc87e80e..124047470 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/ConfigOverrides.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/ConfigOverrides.java @@ -236,7 +236,7 @@ public class ConfigOverrides /* Helper methods /********************************************************************** */ - + protected Map<Class<?>, MutableConfigOverride> _newMap() { return new HashMap<Class<?>, MutableConfigOverride>(); } diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MutableCoercionConfig.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MutableCoercionConfig.java new file mode 100644 index 000000000..479ff770c --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MutableCoercionConfig.java @@ -0,0 +1,36 @@ +package com.fasterxml.jackson.databind.cfg; + +/** + * Mutable version of {@link CoercionConfig} (or rather, extended API) + * exposed during configuration phase of {@link com.fasterxml.jackson.databind.ObjectMapper} + * construction (via Builder). + * + * @since 2.12 + */ +public class MutableCoercionConfig + extends CoercionConfig + implements java.io.Serializable +{ + private static final long serialVersionUID = 1L; + + public MutableCoercionConfig() { } + + protected MutableCoercionConfig(MutableCoercionConfig src) { + super(src); + } + + public MutableCoercionConfig copy() { + return new MutableCoercionConfig(this); + } + + public MutableCoercionConfig setCoercion(CoercionInputShape shape, + CoercionAction action) { + _coercionsByShape[shape.ordinal()] = action; + return this; + } + + public MutableCoercionConfig setAcceptBlankAsEmpty(Boolean state) { + _acceptBlankAsEmpty = state; + return this; + } +} 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); + } + } + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotatedMethod.java b/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotatedMethod.java index f6d6a997d..77d61e5e6 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotatedMethod.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotatedMethod.java @@ -189,6 +189,14 @@ public final class AnnotatedMethod @Override public String getFullName() { + final String methodName = super.getFullName(); + switch (getParameterCount()) { + case 0: + return methodName+"()"; + case 1: + return methodName+"("+getRawParameterType(0).getName()+")"; + default: + } return String.format("%s(%d params)", super.getFullName(), getParameterCount()); } @@ -215,10 +223,14 @@ public final class AnnotatedMethod * false, otherwise true * * @since 2.4 + * + * @deprecated Since 2.12 (related to [databind#2675]), needs to be configurable */ + @Deprecated public boolean hasReturnType() { Class<?> rt = getRawReturnType(); - return (rt != Void.TYPE && rt != Void.class); + // also, as per [databind#2675], only consider `void` to be real "No return type" + return (rt != Void.TYPE); } /* diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java index 747aeccb5..731102df8 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java @@ -592,6 +592,10 @@ public class JacksonAnnotationIntrospector ArrayList<NamedType> result = new ArrayList<NamedType>(types.length); for (JsonSubTypes.Type type : types) { result.add(new NamedType(type.value(), type.name())); + // [databind#2761]: alternative set of names to use + for (String name : type.names()) { + result.add(new NamedType(type.value(), name)); + } } return result; } diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java index cf70f0c3f..2326cefde 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java @@ -5,7 +5,6 @@ import java.util.*; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.*; @@ -580,10 +579,15 @@ public class POJOPropertiesCollector AnnotatedMethod m, AnnotationIntrospector ai) { // Very first thing: skip if not returning any value - if (!m.hasReturnType()) { - return; + // 06-May-2020, tatu: [databind#2675] changes handling slightly... + { + final Class<?> rt = m.getRawReturnType(); + if ((rt == Void.TYPE) || + ((rt == Void.class) && !_config.isEnabled(MapperFeature.ALLOW_VOID_VALUED_PROPERTIES))) { + return; + } } - + // any getter? // @JsonAnyGetter? if (Boolean.TRUE.equals(ai.hasAnyGetter(m))) { @@ -788,10 +792,9 @@ public class POJOPropertiesCollector while (it.hasNext()) { POJOPropertyBuilder prop = it.next(); // 26-Jan-2017, tatu: [databind#935]: need to denote removal of - JsonProperty.Access acc = prop.removeNonVisible(inferMutators); - if (acc == JsonProperty.Access.READ_ONLY) { - _collectIgnorals(prop.getName()); - } + // 16-May-2020, tatu: [databind#2719]: need to pass `this` to allow + // addition of ignorals wrt explicit name + prop.removeNonVisible(inferMutators, _forSerialization ? null : this); } } @@ -800,9 +803,9 @@ public class POJOPropertiesCollector * of known ignored properties; this helps in proper reporting of * errors. */ - private void _collectIgnorals(String name) + protected void _collectIgnorals(String name) { - if (!_forSerialization) { + if (!_forSerialization && (name != null)) { if (_ignoredPropertyNames == null) { _ignoredPropertyNames = new HashSet<String>(); } @@ -868,12 +871,20 @@ public class POJOPropertiesCollector old.addAll(prop); } // replace the creatorProperty too, if there is one - _updateCreatorProperty(prop, _creatorProperties); - // [databind#2001]: New name of property was ignored previously? Remove from ignored - // 01-May-2018, tatu: I have a feeling this will need to be revisited at some point, - // to avoid removing some types of removals, possibly. But will do for now. - if (_ignoredPropertyNames != null) { - _ignoredPropertyNames.remove(name); + if (_updateCreatorProperty(prop, _creatorProperties)) { + // [databind#2001]: New name of property was ignored previously? Remove from ignored + // 01-May-2018, tatu: I have a feeling this will need to be revisited at some point, + // to avoid removing some types of removals, possibly. But will do for now. + + // 16-May-2020, tatu: ... and so the day came, [databind#2118] failed + // when explicit rename added to ignorals (for READ_ONLY) was suddenly + // removed from ignoral list. So, added a guard statement above so that + // ignoral is ONLY removed if there was matching creator property. + // + // Chances are this is not the last tweak we need but... that bridge then etc + if (_ignoredPropertyNames != null) { + _ignoredPropertyNames.remove(name); + } } } } @@ -1170,16 +1181,17 @@ public class POJOPropertiesCollector _config.canOverrideAccessModifiers()); } - protected void _updateCreatorProperty(POJOPropertyBuilder prop, List<POJOPropertyBuilder> creatorProperties) { + protected boolean _updateCreatorProperty(POJOPropertyBuilder prop, List<POJOPropertyBuilder> creatorProperties) { if (creatorProperties != null) { final String intName = prop.getInternalName(); for (int i = 0, len = creatorProperties.size(); i < len; ++i) { if (creatorProperties.get(i).getInternalName().equals(intName)) { creatorProperties.set(i, prop); - break; + return true; } } } + return false; } } diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertyBuilder.java b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertyBuilder.java index a171f157d..bdc49ae0f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertyBuilder.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertyBuilder.java @@ -216,20 +216,30 @@ public class POJOPropertyBuilder */ @Override - public PropertyMetadata getMetadata() { + public PropertyMetadata getMetadata() + { if (_metadata == null) { - final Boolean b = _findRequired(); - final String desc = _findDescription(); - final Integer idx = _findIndex(); - final String def = _findDefaultValue(); - if (b == null && idx == null && def == null) { - _metadata = (desc == null) ? PropertyMetadata.STD_REQUIRED_OR_OPTIONAL - : PropertyMetadata.STD_REQUIRED_OR_OPTIONAL.withDescription(desc); + // 20-Jun-2020, tatu: Unfortunately strict checks lead to [databind#2757] + // so we will need to try to avoid them at this point + final AnnotatedMember prim = getPrimaryMemberUnchecked(); + + if (prim == null) { + _metadata = PropertyMetadata.STD_REQUIRED_OR_OPTIONAL; } else { - _metadata = PropertyMetadata.construct(b, desc, idx, def); - } - if (!_forSerialization) { - _metadata = _getSetterInfo(_metadata); + final Boolean b = _annotationIntrospector.hasRequiredMarker(prim); + final String desc = _annotationIntrospector.findPropertyDescription(prim); + final Integer idx = _annotationIntrospector.findPropertyIndex(prim); + final String def = _annotationIntrospector.findPropertyDefaultValue(prim); + + if (b == null && idx == null && def == null) { + _metadata = (desc == null) ? PropertyMetadata.STD_REQUIRED_OR_OPTIONAL + : PropertyMetadata.STD_REQUIRED_OR_OPTIONAL.withDescription(desc); + } else { + _metadata = PropertyMetadata.construct(b, desc, idx, def); + } + if (!_forSerialization) { + _metadata = _getSetterInfo(_metadata, prim); + } } } return _metadata; @@ -241,7 +251,8 @@ public class POJOPropertyBuilder * of property value, and handling of incoming nulls. * Only called for deserialization purposes. */ - protected PropertyMetadata _getSetterInfo(PropertyMetadata metadata) + protected PropertyMetadata _getSetterInfo(PropertyMetadata metadata, + AnnotatedMember primary) { boolean needMerge = true; Nulls valueNulls = null; @@ -250,16 +261,13 @@ public class POJOPropertyBuilder // Slightly confusing: first, annotations should be accessed via primary member // (mutator); but accessor is needed for actual merge operation. So - // 20-Jun-2020, tatu: Unfortunately strict checks lead to [databind#2757] - // so we will need to try to avoid them at this point - AnnotatedMember prim = getPrimaryMemberUnchecked(); AnnotatedMember acc = getAccessor(); - if (prim != null) { + if (primary != null) { // Ok, first: does property itself have something to say? if (_annotationIntrospector != null) { if (acc != null) { - Boolean b = _annotationIntrospector.findMergeInfo(prim); + Boolean b = _annotationIntrospector.findMergeInfo(primary); if (b != null) { needMerge = false; if (b.booleanValue()) { @@ -267,7 +275,7 @@ public class POJOPropertyBuilder } } } - JsonSetter.Value setterInfo = _annotationIntrospector.findSetterInfo(prim); + JsonSetter.Value setterInfo = _annotationIntrospector.findSetterInfo(primary); if (setterInfo != null) { valueNulls = setterInfo.nonDefaultValueNulls(); contentNulls = setterInfo.nonDefaultContentNulls(); @@ -279,7 +287,7 @@ public class POJOPropertyBuilder // 20-Jun-2020, tatu: Related to [databind#2757], need to find type // but keeping mind that type for setters is trickier; and that // generic typing gets tricky as well. - Class<?> rawType = _rawTypeOf(prim); + Class<?> rawType = _rawTypeOf(primary); ConfigOverride co = _config.getConfigOverride(rawType); JsonSetter.Value setterInfo = co.getSetterInfo(); if (setterInfo != null) { @@ -694,42 +702,6 @@ public class POJOPropertyBuilder return (b != null) && b.booleanValue(); } - protected Boolean _findRequired() { - return fromMemberAnnotations(new WithMember<Boolean>() { - @Override - public Boolean withMember(AnnotatedMember member) { - return _annotationIntrospector.hasRequiredMarker(member); - } - }); - } - - protected String _findDescription() { - return fromMemberAnnotations(new WithMember<String>() { - @Override - public String withMember(AnnotatedMember member) { - return _annotationIntrospector.findPropertyDescription(member); - } - }); - } - - protected Integer _findIndex() { - return fromMemberAnnotations(new WithMember<Integer>() { - @Override - public Integer withMember(AnnotatedMember member) { - return _annotationIntrospector.findPropertyIndex(member); - } - }); - } - - protected String _findDefaultValue() { - return fromMemberAnnotations(new WithMember<String>() { - @Override - public String withMember(AnnotatedMember member) { - return _annotationIntrospector.findPropertyDefaultValue(member); - } - }); - } - @Override public ObjectIdInfo findObjectIdInfo() { return fromMemberAnnotations(new WithMember<ObjectIdInfo>() { @@ -828,11 +800,19 @@ public class POJOPropertyBuilder _ctorParameters = _removeIgnored(_ctorParameters); } + @Deprecated // since 2.12 + public JsonProperty.Access removeNonVisible(boolean inferMutators) { + return removeNonVisible(inferMutators, null); + } + /** * @param inferMutators Whether mutators can be "pulled in" by visible * accessors or not. + * + * @since 2.12 (earlier had different signature) */ - public JsonProperty.Access removeNonVisible(boolean inferMutators) + public JsonProperty.Access removeNonVisible(boolean inferMutators, + POJOPropertiesCollector parent) { /* 07-Jun-2015, tatu: With 2.6, we will allow optional definition * of explicit access type for property; if not "AUTO", it will @@ -844,6 +824,15 @@ public class POJOPropertyBuilder } switch (acc) { case READ_ONLY: + // [databind#2719]: Need to add ignorals, first, keeping in mind + // we have not yet resolved explicit names, so include implicit + // and possible explicit names + if (parent != null) { + parent._collectIgnorals(getName()); + for (PropertyName pn : findExplicitNames()) { + parent._collectIgnorals(pn.getSimpleName()); + } + } // Remove setters, creators for sure, but fields too if deserializing _setters = null; _ctorParameters = null; diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/TypeDeserializerBase.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/TypeDeserializerBase.java index ea70d4bb1..c8efa858a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/TypeDeserializerBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/TypeDeserializerBase.java @@ -22,9 +22,9 @@ public abstract class TypeDeserializerBase implements java.io.Serializable { private static final long serialVersionUID = 1; - + protected final TypeIdResolver _idResolver; - + protected final JavaType _baseType; /** @@ -47,9 +47,9 @@ public abstract class TypeDeserializerBase * in cases where type id is to be exposed as part of JSON. */ protected final String _typePropertyName; - + protected final boolean _typeIdVisible; - + /** * For efficient operation we will lazily build mappings from type ids * to actual deserializers, once needed. diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/TypeNameIdResolver.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/TypeNameIdResolver.java index 601afb5a2..da4541a5e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/TypeNameIdResolver.java +++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/TypeNameIdResolver.java @@ -164,7 +164,15 @@ public class TypeNameIdResolver extends TypeIdResolverBase @Override public String getDescForKnownTypeIds() { - return new TreeSet<String>(_idToType.keySet()).toString(); + // 05-May-2020, tatu: As per [databind#1919], only include ids for + // non-abstract types + final TreeSet<String> ids = new TreeSet<>(); + for (Map.Entry<String, JavaType> entry : _idToType.entrySet()) { + if (entry.getValue().isConcrete()) { + ids.add(entry.getKey()); + } + } + return ids.toString(); } @Override diff --git a/src/main/java/com/fasterxml/jackson/databind/module/SimpleModule.java b/src/main/java/com/fasterxml/jackson/databind/module/SimpleModule.java index 5de340e5c..9b98f44e6 100644 --- a/src/main/java/com/fasterxml/jackson/databind/module/SimpleModule.java +++ b/src/main/java/com/fasterxml/jackson/databind/module/SimpleModule.java @@ -23,7 +23,7 @@ import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; *<p> * NOTE: although it is not expected that sub-types should need to * override {@link #setupModule(SetupContext)} method, if they choose - * to do so they MUST call <code>super.setupModule(context);</code> + * to do so they MUST call {@code super.setupModule(context);} * to ensure that registration works as expected. *<p> * WARNING: when registering {@link JsonSerializer}s and {@link JsonDeserializer}s, diff --git a/src/main/java/com/fasterxml/jackson/databind/node/TreeTraversingParser.java b/src/main/java/com/fasterxml/jackson/databind/node/TreeTraversingParser.java index 904cb8bed..63200214a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/node/TreeTraversingParser.java +++ b/src/main/java/com/fasterxml/jackson/databind/node/TreeTraversingParser.java @@ -7,6 +7,7 @@ import java.math.BigInteger; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.core.base.ParserMinimalBase; +import com.fasterxml.jackson.core.util.JacksonFeatureSet; import com.fasterxml.jackson.databind.JsonNode; /** @@ -72,7 +73,13 @@ public class TreeTraversingParser extends ParserMinimalBase public Version version() { return com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION; } - + + @Override + public JacksonFeatureSet<StreamReadCapability> getReadCapabilities() { + // Defaults are fine + return DEFAULT_READ_CAPABILITIES; + } + /* /********************************************************** /* Closeable implementation diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java index 4e7c398d2..889a72470 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java @@ -462,15 +462,13 @@ public abstract class BasicSerializerFactory if (Number.class.isAssignableFrom(raw)) { // 21-May-2014, tatu: Couple of alternatives actually JsonFormat.Value format = beanDesc.findExpectedFormat(null); - if (format != null) { - switch (format.getShape()) { - case STRING: - return ToStringSerializer.instance; - case OBJECT: // need to bail out to let it be serialized as POJO - case ARRAY: // or, I guess ARRAY; otherwise no point in speculating - return null; - default: - } + switch (format.getShape()) { + case STRING: + return ToStringSerializer.instance; + case OBJECT: // need to bail out to let it be serialized as POJO + case ARRAY: // or, I guess ARRAY; otherwise no point in speculating + return null; + default: } return NumberSerializer.instance; } @@ -706,7 +704,7 @@ public abstract class BasicSerializerFactory // We may also want to use serialize Collections "as beans", if (and only if) // this is specified with `@JsonFormat(shape=Object)` JsonFormat.Value format = beanDesc.findExpectedFormat(null); - if ((format != null) && format.getShape() == JsonFormat.Shape.OBJECT) { + if (format.getShape() == JsonFormat.Shape.OBJECT) { return null; } Class<?> raw = type.getRawClass(); @@ -796,7 +794,7 @@ public abstract class BasicSerializerFactory // [databind#467]: This is where we could allow serialization "as POJO": But! It's // nasty to undo, and does not apply on per-property basis. So, hardly optimal JsonFormat.Value format = beanDesc.findExpectedFormat(null); - if ((format != null) && format.getShape() == JsonFormat.Shape.OBJECT) { + if (format.getShape() == JsonFormat.Shape.OBJECT) { return null; } @@ -1195,7 +1193,7 @@ public abstract class BasicSerializerFactory * otherwise pass it to EnumSerializer. */ JsonFormat.Value format = beanDesc.findExpectedFormat(null); - if (format != null && format.getShape() == JsonFormat.Shape.OBJECT) { + if (format.getShape() == JsonFormat.Shape.OBJECT) { // one special case: suppress serialization of "getDeclaringClass()"... ((BasicBeanDescription) beanDesc).removeProperty("declaringClass"); // returning null will mean that eventually BeanSerializer gets constructed diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/BeanSerializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/ser/BeanSerializerFactory.java index 79c9a96f0..07a969208 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/BeanSerializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/BeanSerializerFactory.java @@ -16,9 +16,11 @@ import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.ser.impl.FilteredBeanPropertyWriter; import com.fasterxml.jackson.databind.ser.impl.ObjectIdWriter; import com.fasterxml.jackson.databind.ser.impl.PropertyBasedObjectIdGenerator; +import com.fasterxml.jackson.databind.ser.impl.UnsupportedTypeSerializer; import com.fasterxml.jackson.databind.ser.std.MapSerializer; import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer; import com.fasterxml.jackson.databind.type.ReferenceType; +import com.fasterxml.jackson.databind.util.BeanUtil; import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.jackson.databind.util.Converter; @@ -376,6 +378,12 @@ public class BeanSerializerFactory return prov.getUnknownTypeSerializer(Object.class); // throw new IllegalArgumentException("Cannot create bean serializer for Object.class"); } + + JsonSerializer<?> ser = _findUnsupportedTypeSerializer(prov, type, beanDesc); + if (ser != null) { + return (JsonSerializer<Object>) ser; + } + final SerializationConfig config = prov.getConfig(); BeanSerializerBuilder builder = constructBeanSerializerBuilder(beanDesc); builder.setConfig(config); @@ -447,9 +455,8 @@ public class BeanSerializerFactory } } - JsonSerializer<Object> ser = null; try { - ser = (JsonSerializer<Object>) builder.build(); + ser = builder.build(); } catch (RuntimeException e) { return prov.reportBadTypeDefinition(beanDesc, "Failed to construct BeanSerializer for %s: (%s) %s", beanDesc.getType(), e.getClass().getName(), e.getMessage()); @@ -467,7 +474,7 @@ public class BeanSerializerFactory } } } - return ser; + return (JsonSerializer<Object>) ser; } protected ObjectIdWriter constructObjectIdHandler(SerializerProvider prov, @@ -528,7 +535,7 @@ public class BeanSerializerFactory { return FilteredBeanPropertyWriter.constructViewBased(writer, inViews); } - + protected PropertyBuilder constructPropertyBuilder(SerializationConfig config, BeanDescription beanDesc) { @@ -538,7 +545,7 @@ public class BeanSerializerFactory protected BeanSerializerBuilder constructBeanSerializerBuilder(BeanDescription beanDesc) { return new BeanSerializerBuilder(beanDesc); } - + /* /********************************************************** /* Overridable non-public introspection methods @@ -814,4 +821,15 @@ public class BeanSerializerFactory return pb.buildWriter(prov, propDef, type, annotatedSerializer, typeSer, contentTypeSer, accessor, staticTyping); } + + protected JsonSerializer<?> _findUnsupportedTypeSerializer(SerializerProvider 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'? + final String errorMsg = BeanUtil.checkUnsupportedType(type); + return (errorMsg == null) ? null + : new UnsupportedTypeSerializer(type, errorMsg); + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/impl/FailingSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/impl/FailingSerializer.java index 6021fa89e..e047c6010 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/impl/FailingSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/impl/FailingSerializer.java @@ -1,14 +1,11 @@ package com.fasterxml.jackson.databind.ser.impl; import java.io.IOException; -import java.lang.reflect.Type; import com.fasterxml.jackson.core.*; -import com.fasterxml.jackson.databind.JavaType; + import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; import com.fasterxml.jackson.databind.ser.std.StdSerializer; /** @@ -30,19 +27,8 @@ public class FailingSerializer } @Override - public void serialize(Object value, JsonGenerator g, SerializerProvider provider) throws IOException - { - provider.reportMappingProblem(_msg); - } - - @Override - public JsonNode getSchema(SerializerProvider provider, Type typeHint) throws JsonMappingException { - return null; - } - - @Override - public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) + public void serialize(Object value, JsonGenerator g, SerializerProvider ctxt) throws IOException { - ; + ctxt.reportMappingProblem(_msg); } } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/impl/PropertySerializerMap.java b/src/main/java/com/fasterxml/jackson/databind/ser/impl/PropertySerializerMap.java index e49bf5883..435701880 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/impl/PropertySerializerMap.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/impl/PropertySerializerMap.java @@ -165,14 +165,6 @@ public abstract class PropertySerializerMap public abstract PropertySerializerMap newWith(Class<?> type, JsonSerializer<Object> serializer); /** - * @deprecated Since 2.5 Use {@link #emptyForProperties} instead - */ - @Deprecated - public static PropertySerializerMap emptyMap() { - return emptyForProperties(); - } - - /** * @since 2.5 */ public static PropertySerializerMap emptyForProperties() { diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/impl/UnsupportedTypeSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/impl/UnsupportedTypeSerializer.java new file mode 100644 index 000000000..b849ee3d2 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/ser/impl/UnsupportedTypeSerializer.java @@ -0,0 +1,37 @@ +package com.fasterxml.jackson.databind.ser.impl; + +import java.io.IOException; + +import com.fasterxml.jackson.core.*; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +/** + * Special bogus "serializer" that will throw + * {@link com.fasterxml.jackson.databind.exc.InvalidDefinitionException} if its {@link #serialize} + * gets invoked. Most commonly registered as handler for unknown types, + * as well as for catching unintended usage (like trying to use null + * as Map/Object key). + */ +public class UnsupportedTypeSerializer + extends StdSerializer<Object> +{ + private static final long serialVersionUID = 1L; + + protected final JavaType _type; + + protected final String _message; + + public UnsupportedTypeSerializer(JavaType t, String msg) { + super(Object.class); + _type = t; + _message = msg; + } + + @Override + public void serialize(Object value, JsonGenerator g, SerializerProvider ctxt) throws IOException { + ctxt.reportBadDefinition(_type, _message); + } +} diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/BeanSerializerBase.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/BeanSerializerBase.java index 4632e5142..95621c9a5 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/BeanSerializerBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/BeanSerializerBase.java @@ -128,8 +128,8 @@ public abstract class BeanSerializerBase _anyGetterWriter = builder.getAnyGetter(); _propertyFilterId = builder.getFilterId(); _objectIdWriter = builder.getObjectIdWriter(); - JsonFormat.Value format = builder.getBeanDescription().findExpectedFormat(null); - _serializationShape = (format == null) ? null : format.getShape(); + final JsonFormat.Value format = builder.getBeanDescription().findExpectedFormat(null); + _serializationShape = format.getShape(); } } @@ -265,14 +265,8 @@ public abstract class BeanSerializerBase * * @since 2.11.1 */ - protected BeanSerializerBase withProperties(BeanPropertyWriter[] properties, - BeanPropertyWriter[] filteredProperties) { - return this; - } - - // Should be this, will be with 2.12: -// protected abstract BeanSerializerBase withProperties(BeanPropertyWriter[] properties, -// BeanPropertyWriter[] filteredProperties); + protected abstract BeanSerializerBase withProperties(BeanPropertyWriter[] properties, + BeanPropertyWriter[] filteredProperties); /** * Copy-constructor that is useful for sub-classes that just want to diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/StdScalarSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/StdScalarSerializer.java index 916a9c980..683a2f40f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/StdScalarSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/StdScalarSerializer.java @@ -28,7 +28,18 @@ public abstract class StdScalarSerializer<T> protected StdScalarSerializer(Class<?> t, boolean dummy) { super((Class<T>) t); } - + + /** + * Basic copy-constructor + * + * @param src Original instance to copy settings from + * + * @since 2.12 + */ + protected StdScalarSerializer(StdScalarSerializer<?> src) { + super(src); + } + /** * Default implementation will write type prefix, call regular serialization * method (since assumption is that value itself does not need JSON diff --git a/src/main/java/com/fasterxml/jackson/databind/type/ArrayType.java b/src/main/java/com/fasterxml/jackson/databind/type/ArrayType.java index ef3e6aafb..901b342fe 100644 --- a/src/main/java/com/fasterxml/jackson/databind/type/ArrayType.java +++ b/src/main/java/com/fasterxml/jackson/databind/type/ArrayType.java @@ -197,7 +197,20 @@ public final class ArrayType sb.append('['); return _componentType.getErasedSignature(sb); } - + + /* + /********************************************************** + /* Extended API + /********************************************************** + */ + + /** + * @since 2.12 + */ + public Object[] getEmptyArray() { + return (Object[]) _emptyArray; + } + /* /********************************************************** /* Standard methods diff --git a/src/main/java/com/fasterxml/jackson/databind/type/LogicalType.java b/src/main/java/com/fasterxml/jackson/databind/type/LogicalType.java new file mode 100644 index 000000000..a04d21d6d --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/type/LogicalType.java @@ -0,0 +1,136 @@ +package com.fasterxml.jackson.databind.type; + +/** + * Set of logical types (or type categories, classes of classes), used + * for defining applicability of configuration like coercion configuration. + * Used instead to allow easier targeting of types than having to enumerate + * physical types ({@link java.lang.Class} or {@link com.fasterxml.jackson.databind.JavaType}). + * + * @since 2.12 + */ +public enum LogicalType +{ + // // // General container types + + /** + * Array types of other values. + *<p> + * Note: excludes binary type {@code byte[]}. + */ + Array, + + /** + * {@link java.util.Collection} values (and "Collection-like" for JVM + * languages and datatype libraries with semantically similar types) + */ + Collection, + + /** + * {@link java.util.Map} values (and "Map-like" for JVM + * languages and datatype libraries with semantically similar types) + */ + Map, + + // // // Other structured java types + + /** + * Types that are handled by default "set of key/value pairs" serialization, + * also known as "Beans". + *<p> + * In addition to user-defined types, also includes JDK types like: + *<ul> + * <li>{@link java.lang.Throwable} + * </li> + * </ul> + */ + POJO, + + /** + * "Non-type", Type used to contained untyped, free-form content: maybe + * a "Tree" (sometimes called "AST"), or buffer of some kind, + * or even just nominal type of {@link java.lang.Object} + */ + Untyped, + + // // // Basic scalar types + + /** + * Basic integral numbers types like {@code short}, {@code int}, {@code long} + * and matching wrapper types, {@link java.math.BigInteger}. + */ + Integer, + + /** + * Basic floating-point numbers types like {@code short}, {@code int}, {@code long} + * and matching wrapper types, {@link java.math.BigInteger}. + */ + Float, + + /** + * {@link java.lang.Boolean}, {@code boolean}, {@link java.util.concurrent.atomic.AtomicBoolean}. + */ + Boolean, + + /** + * Various {@link java.lang.Enum} types. + */ + Enum, + + /** + * Purely textual types, {@link java.lang.String} and similar (but not types that + * are generally expressed as Strings in input). + */ + Textual, + + /** + * Binary data such as {@code byte[]} and {@link java.nio.ByteBuffer}. + */ + Binary, + + /** + * Date/time datatypes such as {@link java.util.Date}, {@link java.util.Calendar}. + */ + DateTime, + + /** + * Scalar types other than ones listed above: includes types like {@link java.net.URL} + * and {@link java.util.UUID}. + */ + OtherScalar + ; + + /** + * Helper method to use for figuring out logical type from physical type, + * in cases where caller wants a guess. Note that introspection is + * not exhaustive and mostly covers basic {@link java.util.Collection}, + * {@link java.util.Map} and {@link java.lang.Enum} cases; but not + * more specific types (for example datatype-provided extension types). + * + * @param raw Type-erased class to classify + * @param defaultIfNotRecognized if no type recognized, value to return + * (for example, {@code null}) + */ + public static LogicalType fromClass(Class<?> raw, + LogicalType defaultIfNotRecognized) + { + if (raw.isEnum()) { + return Enum; + } + if (raw.isArray()) { + if (raw == byte[].class) { + return Binary; + } + return Array; + } + if (java.util.Collection.class.isAssignableFrom(raw)) { + return Collection; + } + if (java.util.Map.class.isAssignableFrom(raw)) { + return Map; + } + if (raw == String.class) { + return Textual; + } + return defaultIfNotRecognized; + } +} diff --git a/src/main/java/com/fasterxml/jackson/databind/type/TypeFactory.java b/src/main/java/com/fasterxml/jackson/databind/type/TypeFactory.java index 147900057..e424690d3 100644 --- a/src/main/java/com/fasterxml/jackson/databind/type/TypeFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/type/TypeFactory.java @@ -698,6 +698,14 @@ public class TypeFactory // note: was final in 2.9, removed from 2.10 return _fromAny(null, type, EMPTY_BINDINGS); } + /** + * Method that you very likely should NOT be using -- you need to know a lot + * about internal details of {@link TypeBindings} and even then it will probably + * not do what you want. + * Usually you would instead want to call one of {@code constructXxxType()} + * methods (where {@code Xxx} would be "Array", "Collection[Like]", "Map[Like]" + * or "Parametric"). + */ public JavaType constructType(Type type, TypeBindings bindings) { // 15-Jun-2020, tatu: To resolve (parts of) [databind#2796], need to // call _fromClass() directly if we get `Class` argument @@ -966,7 +974,8 @@ public class TypeFactory // note: was final in 2.9, removed from 2.10 */ public JavaType constructReferenceType(Class<?> rawType, JavaType referredType) { - return ReferenceType.construct(rawType, null, // no bindings + return ReferenceType.construct(rawType, + TypeBindings.create(rawType, referredType), // [databind#2091] null, null, // or super-class, interfaces? referredType); } @@ -1052,9 +1061,32 @@ public class TypeFactory // note: was final in 2.9, removed from 2.10 */ public JavaType constructParametricType(Class<?> rawType, JavaType... parameterTypes) { + return constructParametricType(rawType, TypeBindings.create(rawType, parameterTypes)); + } + + /** + * Factory method for constructing {@link JavaType} that + * represents a parameterized type. The type's parameters are + * specified as an instance of {@link TypeBindings}. This + * is useful if you already have the type's parameters such + * as those found on {@link JavaType}. For example, you could + * call + * <pre> + * return TypeFactory.constructParametricType(ArrayList.class, javaType.getBindings()); + * </pre> + * This effectively applies the parameterized types from one + * {@link JavaType} to another class. + * + * @param rawType Actual type-erased type + * @param parameterTypes Type bindings for the raw type + * + * @since 2.12 + */ + public JavaType constructParametricType(Class<?> rawType, TypeBindings parameterTypes) + { // 16-Jul-2020, tatu: Since we do not call `_fromAny()`, need to make // sure `TypeModifier`s are applied: - JavaType resultType = _fromClass(null, rawType, TypeBindings.create(rawType, parameterTypes)); + JavaType resultType = _fromClass(null, rawType, parameterTypes); return _applyModifiers(rawType, resultType); } diff --git a/src/main/java/com/fasterxml/jackson/databind/util/BeanUtil.java b/src/main/java/com/fasterxml/jackson/databind/util/BeanUtil.java index d381cdbf0..c58930bf6 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/BeanUtil.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/BeanUtil.java @@ -289,4 +289,50 @@ public class BeanUtil sb.append(basename, offset+1, end); return sb.toString(); } + + /* + /********************************************************** + /* Package-specific type detection for error handling + /********************************************************** + */ + + /** + * Helper method called by {@link com.fasterxml.jackson.databind.deser.BeanDeserializerFactory} + * and {@link com.fasterxml.jackson.databind.ser.BeanSerializerFactory} to check + * if given unrecognized type (to be (de)serialized as general POJO) is one of + * "well-known" types for which there would be a datatype module; and if so, + * return appropriate failure message to give to caller. + * + * @since 2.12 + */ + public static String checkUnsupportedType(JavaType type) { + final Class<?> rawType = type.getRawClass(); + String typeName, moduleName; + + if (isJava8TimeClass(rawType)) { + typeName = "Java 8 date/time"; + moduleName = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"; + } else if (isJodaTimeClass(rawType)) { + typeName = "Joda date/time"; + moduleName = "com.fasterxml.jackson.datatype:jackson-datatype-joda"; + } else { + return null; + } + return String.format("%s type %s not supported by default: add Module \"%s\" to enable handling", + typeName, ClassUtil.getTypeDescription(type), moduleName); + } + + /** + * @since 2.12 + */ + public static boolean isJava8TimeClass(Class<?> rawType) { + return rawType.getName().startsWith("java.time."); + } + + /** + * @since 2.12 + */ + public static boolean isJodaTimeClass(Class<?> rawType) { + return rawType.getName().startsWith("org.joda.time."); + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/util/StdDateFormat.java b/src/main/java/com/fasterxml/jackson/databind/util/StdDateFormat.java index 84a7feee3..3f6577ebe 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/StdDateFormat.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/StdDateFormat.java @@ -306,9 +306,8 @@ public class StdDateFormat @Override public void setTimeZone(TimeZone tz) { - /* DateFormats are timezone-specific (via Calendar contained), - * so need to reset instances if timezone changes: - */ + // DateFormats are timezone-specific (via Calendar contained), + // so need to reset instances if timezone changes: if (!tz.equals(_timezone)) { _clearFormats(); _timezone = tz; diff --git a/src/main/java/com/fasterxml/jackson/databind/util/TokenBuffer.java b/src/main/java/com/fasterxml/jackson/databind/util/TokenBuffer.java index e37835f64..8fa86e7df 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/TokenBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/TokenBuffer.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.core.base.ParserMinimalBase; import com.fasterxml.jackson.core.json.JsonWriteContext; import com.fasterxml.jackson.core.util.ByteArrayBuilder; +import com.fasterxml.jackson.core.util.JacksonFeatureSet; import com.fasterxml.jackson.databind.*; /** @@ -639,7 +640,16 @@ sb.append("NativeObjectIds=").append(_hasNativeObjectIds).append(","); public boolean canWriteBinaryNatively() { return true; } - + + // 20-May-2020, tatu: This may or may not be enough -- ideally access is + // via `DeserializationContext`, not parser, but if latter is needed + // then we'll need to pass this from parser contents if which were + // buffered. + @Override + public JacksonFeatureSet<StreamWriteCapability> getWriteCapabilities() { + return DEFAULT_WRITE_CAPABILITIES; + } + /* /********************************************************** /* JsonGenerator implementation: low-level output handling @@ -671,19 +681,11 @@ sb.append("NativeObjectIds=").append(_hasNativeObjectIds).append(","); _writeContext = _writeContext.createChildArrayContext(); } - @Override // since 2.10 (method added in 2.4) - public final void writeStartArray(int size) throws IOException - { - _writeContext.writeValue(); - _appendStartMarker(JsonToken.START_ARRAY); - _writeContext = _writeContext.createChildArrayContext(); - } - @Override // since 2.10.1 public void writeStartArray(Object forValue) throws IOException { _writeContext.writeValue(); _appendStartMarker(JsonToken.START_ARRAY); - _writeContext = _writeContext.createChildArrayContext(); + _writeContext = _writeContext.createChildArrayContext(forValue); } @Override // since 2.10.1 @@ -1226,16 +1228,10 @@ sb.append("NativeObjectIds=").append(_hasNativeObjectIds).append(","); if (_forceBigDecimal) { writeNumber(p.getDecimalValue()); } else { - switch (p.getNumberType()) { - case BIG_DECIMAL: - writeNumber(p.getDecimalValue()); - break; - case FLOAT: - writeNumber(p.getFloatValue()); - break; - default: - writeNumber(p.getDoubleValue()); - } + // 09-Jul-2020, tatu: Used to just copy using most optimal method, but + // issues like [databind#2644] force to use exact, not optimal type + final Number n = p.getNumberValueExact(); + _appendValue(JsonToken.VALUE_NUMBER_FLOAT, n); } break; case VALUE_TRUE: @@ -1505,18 +1501,33 @@ sb.append("NativeObjectIds=").append(_hasNativeObjectIds).append(","); public void setLocation(JsonLocation l) { _location = l; } - + @Override public ObjectCodec getCodec() { return _codec; } @Override public void setCodec(ObjectCodec c) { _codec = c; } + /* + /********************************************************** + /* Public API, config access, capability introspection + /********************************************************** + */ + @Override public Version version() { return com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION; } + // 20-May-2020, tatu: This may or may not be enough -- ideally access is + // via `DeserializationContext`, not parser, but if latter is needed + // then we'll need to pass this from parser contents if which were + // buffered. + @Override + public JacksonFeatureSet<StreamReadCapability> getReadCapabilities() { + return DEFAULT_READ_CAPABILITIES; + } + /* /********************************************************** /* Extended API beyond JsonParser diff --git a/src/test-jdk14/java/com/fasterxml/jackson/databind/RecordTest.java b/src/test-jdk14/java/com/fasterxml/jackson/databind/RecordTest.java new file mode 100644 index 000000000..7e80ddb5f --- /dev/null +++ b/src/test-jdk14/java/com/fasterxml/jackson/databind/RecordTest.java @@ -0,0 +1,105 @@ +package com.fasterxml.jackson.databind; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.json.JsonMapper; + +import java.io.IOException; + +public class RecordTest extends BaseMapTest { + + private JsonMapper jsonMapper; + + public void setUp() { + jsonMapper = new JsonMapper(); + } + + record SimpleRecord(int id, String name) { + } + + public void testSerializeSimpleRecord() throws JsonProcessingException { + SimpleRecord record = new SimpleRecord(123, "Bob"); + + String json = jsonMapper.writeValueAsString(record); + + assertEquals("{\"id\":123,\"name\":\"Bob\"}", json); + } + + public void testDeserializeSimpleRecord() throws IOException { + SimpleRecord value = jsonMapper.readValue("{\"id\":123,\"name\":\"Bob\"}", SimpleRecord.class); + + assertEquals(new SimpleRecord(123, "Bob"), value); + } + + public void testSerializeSimpleRecord_DisableAnnotationIntrospector() throws JsonProcessingException { + SimpleRecord record = new SimpleRecord(123, "Bob"); + + JsonMapper mapper = JsonMapper.builder() + .configure(MapperFeature.USE_ANNOTATIONS, false) + .build(); + String json = mapper.writeValueAsString(record); + + assertEquals("{\"id\":123,\"name\":\"Bob\"}", json); + } + + public void testDeserializeSimpleRecord_DisableAnnotationIntrospector() throws IOException { + JsonMapper mapper = JsonMapper.builder() + .configure(MapperFeature.USE_ANNOTATIONS, false) + .build(); + SimpleRecord value = mapper.readValue("{\"id\":123,\"name\":\"Bob\"}", SimpleRecord.class); + + assertEquals(new SimpleRecord(123, "Bob"), value); + } + + record RecordOfRecord(SimpleRecord record) { + } + + public void testSerializeRecordOfRecord() throws JsonProcessingException { + RecordOfRecord record = new RecordOfRecord(new SimpleRecord(123, "Bob")); + + String json = jsonMapper.writeValueAsString(record); + + assertEquals("{\"record\":{\"id\":123,\"name\":\"Bob\"}}", json); + } + + record JsonIgnoreRecord(int id, @JsonIgnore String name) { + } + + public void testSerializeJsonIgnoreRecord() throws JsonProcessingException { + JsonIgnoreRecord record = new JsonIgnoreRecord(123, "Bob"); + + String json = jsonMapper.writeValueAsString(record); + + assertEquals("{\"id\":123}", json); + } + + record RecordWithConstructor(int id, String name) { + public RecordWithConstructor(int id) { + this(id, "name"); + } + } + + public void testDeserializeRecordWithConstructor() throws IOException { + RecordWithConstructor value = jsonMapper.readValue("{\"id\":123,\"name\":\"Bob\"}", RecordWithConstructor.class); + + assertEquals(new RecordWithConstructor(123, "Bob"), value); + } + + record JsonPropertyRenameRecord(int id, @JsonProperty("rename")String name) { + } + + public void testSerializeJsonRenameRecord() throws JsonProcessingException { + JsonPropertyRenameRecord record = new JsonPropertyRenameRecord(123, "Bob"); + + String json = jsonMapper.writeValueAsString(record); + + assertEquals("{\"id\":123,\"rename\":\"Bob\"}", json); + } + + public void testDeserializeJsonRenameRecord() throws IOException { + JsonPropertyRenameRecord value = jsonMapper.readValue("{\"id\":123,\"rename\":\"Bob\"}", JsonPropertyRenameRecord.class); + + assertEquals(new JsonPropertyRenameRecord(123, "Bob"), value); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/BaseMapTest.java b/src/test/java/com/fasterxml/jackson/databind/BaseMapTest.java index 549e96028..13aca9c95 100644 --- a/src/test/java/com/fasterxml/jackson/databind/BaseMapTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/BaseMapTest.java @@ -359,6 +359,10 @@ public abstract class BaseMapTest return json.replace("'", "\""); } + protected static String a2q(String json) { + return json.replace("'", "\""); + } + protected static String quotesToApos(String json) { return json.replace("\"", "'"); } diff --git a/src/test/java/com/fasterxml/jackson/databind/ObjectMapperTest.java b/src/test/java/com/fasterxml/jackson/databind/ObjectMapperTest.java index 7e39256c5..9d84df29a 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ObjectMapperTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/ObjectMapperTest.java @@ -272,7 +272,6 @@ public class ObjectMapperTest extends BaseMapTest assertTrue(dc.shouldSortPropertiesAlphabetically()); } - public void testJsonFactoryLinkage() { // first, implicit factory, giving implicit linkage @@ -363,6 +362,13 @@ public class ObjectMapperTest extends BaseMapTest .canSerialize(EmptyBean.class)); } + // for [databind#2749]: just to check there's no NPE; method really not useful + public void testCanDeserialize() + { + assertTrue(MAPPER.canDeserialize(MAPPER.constructType(EmptyBean.class))); + assertTrue(MAPPER.canDeserialize(MAPPER.constructType(Object.class))); + } + // for [databind#898] public void testSerializerProviderAccess() throws Exception { diff --git a/src/test/java/com/fasterxml/jackson/databind/convert/CoerceContainersTest.java b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceContainersTest.java new file mode 100644 index 000000000..163588155 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceContainersTest.java @@ -0,0 +1,175 @@ +package com.fasterxml.jackson.databind.convert; + +import java.util.*; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.*; + +import com.fasterxml.jackson.databind.cfg.CoercionAction; +import com.fasterxml.jackson.databind.cfg.CoercionInputShape; + +public class CoerceContainersTest extends BaseMapTest +{ + private final String JSON_EMPTY = quote(""); + + private final ObjectMapper VANILLA_MAPPER = sharedMapper(); + + private final ObjectMapper COERCING_MAPPER = newJsonMapper(); + { + COERCING_MAPPER.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty); + } + + /* + /******************************************************** + /* Tests for collections + /******************************************************** + */ + + public void testScalarCollections() throws Exception + { + final JavaType listType = VANILLA_MAPPER.getTypeFactory() + .constructType(new TypeReference<List<Double>>() { }); + _verifyNoCoercion(listType); + List<Double> result = _readWithCoercion(listType); + assertNotNull(result); + assertEquals(0, result.size()); + } + + public void testStringCollections() throws Exception + { + final JavaType listType = VANILLA_MAPPER.getTypeFactory() + .constructType(new TypeReference<List<String>>() { }); + _verifyNoCoercion(listType); + List<String> result = _readWithCoercion(listType); + assertNotNull(result); + assertEquals(0, result.size()); + } + + /* + /******************************************************** + /* Tests for Maps + /******************************************************** + */ + + public void testScalarMap() throws Exception + { + final JavaType mapType = VANILLA_MAPPER.getTypeFactory() + .constructType(new TypeReference<Map<Long, Boolean>>() { }); + _verifyNoCoercion(mapType); + Map<?,?> result = _readWithCoercion(mapType); + assertNotNull(result); + assertEquals(0, result.size()); + } + + public void testEnumMap() throws Exception + { + final JavaType mapType = VANILLA_MAPPER.getTypeFactory() + .constructType(new TypeReference<EnumMap<ABC, Boolean>>() { }); + _verifyNoCoercion(mapType); + Map<?,?> result = _readWithCoercion(mapType); + assertNotNull(result); + assertEquals(0, result.size()); + } + + /* + /******************************************************** + /* Tests for arrays + /******************************************************** + */ + + public void testObjectArray() throws Exception + { + final JavaType arrayType = VANILLA_MAPPER.getTypeFactory() + .constructType(new TypeReference<Object[]>() { }); + _verifyNoCoercion(arrayType); + Object[] result = _readWithCoercion(arrayType); + assertNotNull(result); + assertEquals(0, result.length); + } + + public void testStringArray() throws Exception + { + final JavaType arrayType = VANILLA_MAPPER.getTypeFactory() + .constructType(new TypeReference<String[]>() { }); + _verifyNoCoercion(arrayType); + String[] result = _readWithCoercion(arrayType); + assertNotNull(result); + assertEquals(0, result.length); + } + + public void testBooleanArray() throws Exception + { + _verifyNoCoercion(boolean[].class); + boolean[] result = _readWithCoercion(boolean[].class); + assertNotNull(result); + assertEquals(0, result.length); + } + + public void testIntArray() throws Exception + { + _verifyNoCoercion(int[].class); + int[] result = _readWithCoercion(int[].class); + assertNotNull(result); + assertEquals(0, result.length); + } + + public void testLongArray() throws Exception + { + _verifyNoCoercion(long[].class); + long[] result = _readWithCoercion(long[].class); + assertNotNull(result); + assertEquals(0, result.length); + } + + public void testFloatArray() throws Exception + { + _verifyNoCoercion(float[].class); + float[] result = _readWithCoercion(float[].class); + assertNotNull(result); + assertEquals(0, result.length); + } + + public void testDoubleArray() throws Exception + { + _verifyNoCoercion(double[].class); + double[] result = _readWithCoercion(double[].class); + assertNotNull(result); + assertEquals(0, result.length); + } + + public void testPOJOArray() throws Exception + { + _verifyNoCoercion(StringWrapper[].class); + StringWrapper[] result = _readWithCoercion(StringWrapper[].class); + assertNotNull(result); + assertEquals(0, result.length); + } + + /* + /******************************************************** + /* Helper methods + /******************************************************** + */ + + private void _verifyNoCoercion(Class<?> targetType) throws Exception { + _verifyNoCoercion(VANILLA_MAPPER.constructType(targetType)); + } + + private void _verifyNoCoercion(JavaType targetType) throws Exception { + try { + VANILLA_MAPPER.readerFor(targetType).readValue(JSON_EMPTY); + fail("Should not pass"); + } catch (Exception e) { + verifyException(e, "Cannot deserialize value of type"); + verifyException(e, "from empty String"); + } + } + + private <T> T _readWithCoercion(Class<?> targetType) throws Exception { + return COERCING_MAPPER.readerFor(targetType).readValue(JSON_EMPTY); + } + + private <T> T _readWithCoercion(JavaType targetType) throws Exception { + return COERCING_MAPPER.readerFor(targetType).readValue(JSON_EMPTY); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/convert/CoerceEmptyArrayTest.java b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceEmptyArrayTest.java new file mode 100644 index 000000000..ff429b8ae --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceEmptyArrayTest.java @@ -0,0 +1,255 @@ +package com.fasterxml.jackson.databind.convert; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URL; +import java.util.*; + +import com.fasterxml.jackson.core.type.TypeReference; +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.MismatchedInputException; +import com.fasterxml.jackson.databind.type.LogicalType; + +/** + * Tests to verify implementation of [databind#540]; also for + * follow up work of: + * + * - [databind#994] + */ +public class CoerceEmptyArrayTest extends BaseMapTest +{ + private final ObjectMapper DEFAULT_MAPPER = sharedMapper(); + private final ObjectReader DEFAULT_READER = DEFAULT_MAPPER.reader(); + private final ObjectReader READER_WITH_ARRAYS = DEFAULT_READER + .with(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT); + + private final ObjectMapper MAPPER_TO_EMPTY; + { + MAPPER_TO_EMPTY = newJsonMapper(); + MAPPER_TO_EMPTY.coercionConfigDefaults() + .setCoercion(CoercionInputShape.EmptyArray, CoercionAction.AsEmpty); + } + + private final ObjectMapper MAPPER_TRY_CONVERT; + { + MAPPER_TRY_CONVERT = newJsonMapper(); + MAPPER_TRY_CONVERT.coercionConfigDefaults() + .setCoercion(CoercionInputShape.EmptyArray, CoercionAction.TryConvert); + } + + private final ObjectMapper MAPPER_TO_NULL; + { + MAPPER_TO_NULL = newJsonMapper(); + MAPPER_TO_NULL.coercionConfigDefaults() + .setCoercion(CoercionInputShape.EmptyArray, CoercionAction.AsNull); + } + + private final ObjectMapper MAPPER_TO_FAIL; + { + MAPPER_TO_FAIL = newJsonMapper(); + MAPPER_TO_FAIL.coercionConfigDefaults() + .setCoercion(CoercionInputShape.EmptyArray, CoercionAction.Fail); + } + + static class Bean { + public String a = "foo"; + + @Override + public boolean equals(Object o) { + return (o instanceof Bean) + && a.equals(((Bean) o).a); + } + } + + final static String EMPTY_ARRAY = " [\n]"; + + /* + /********************************************************** + /* Test methods, settings + /********************************************************** + */ + + public void testSettings() { + assertFalse(DEFAULT_MAPPER.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)); + assertFalse(DEFAULT_READER.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)); + assertTrue(READER_WITH_ARRAYS.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)); + } + + /* + /********************************************************** + /* Test methods, POJOs + /********************************************************** + */ + + // [databind#540] + public void testPOJOFromEmptyArray() throws Exception + { + final Class<?> targetType = Bean.class; + + _verifyFailForEmptyArray(DEFAULT_READER, targetType); + _verifyFailForEmptyArray(MAPPER_TO_FAIL, targetType); + + // Nulls for explicit, "TryConvert" + _verifyToNullCoercion(MAPPER_TO_NULL, targetType); + _verifyToNullCoercion(MAPPER_TRY_CONVERT, targetType); + + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, targetType, new Bean()); + + // But let's also check precedence: legacy setting allow, but mask for type + ObjectMapper mapper = jsonMapperBuilder() + .enable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) + .build(); + mapper.coercionConfigFor(targetType) + .setCoercion(CoercionInputShape.EmptyArray, CoercionAction.Fail); + _verifyFailForEmptyArray(mapper, targetType); + + // and conversely + mapper = jsonMapperBuilder() + .disable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) + .build(); + mapper.coercionConfigFor(LogicalType.POJO) + .setCoercion(CoercionInputShape.EmptyArray, CoercionAction.AsEmpty); + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, targetType, new Bean()); + } + + /* + /********************************************************** + /* Test methods, Maps + /********************************************************** + */ + + public void testMapFromEmptyArray() throws Exception + { + final Class<?> targetType = Map.class; + + _verifyFailForEmptyArray(DEFAULT_READER, targetType); + _verifyFailForEmptyArray(MAPPER_TO_FAIL, targetType); + + // Nulls for explicit, "TryConvert" + _verifyToNullCoercion(MAPPER_TO_NULL, targetType); + _verifyToNullCoercion(MAPPER_TRY_CONVERT, targetType); + + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, targetType, new LinkedHashMap<>()); + + // assume overrides work ok since POJOs test it + } + + public void testEnumMapFromEmptyArray() throws Exception + { + final JavaType targetType = DEFAULT_READER.getTypeFactory() + .constructType(new TypeReference<EnumMap<ABC,String>>() { }); + + assertNull(MAPPER_TO_NULL.readerFor(targetType).readValue(EMPTY_ARRAY)); + + EnumMap<?,?> result = MAPPER_TO_EMPTY.readerFor(targetType).readValue(EMPTY_ARRAY); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + /* + /********************************************************** + /* Test methods, scalars + /********************************************************** + */ + + public void testNumbersFromEmptyArray() throws Exception + { + for (Class<?> targetType : new Class<?>[] { + Boolean.class, Character.class, + Byte.class, Short.class, Integer.class, Long.class, + Float.class, Double.class, + BigInteger.class, BigDecimal.class + }) { + // Default, fail; explicit fail + _verifyFailForEmptyArray(DEFAULT_READER, targetType); + _verifyFailForEmptyArray(MAPPER_TO_FAIL, targetType); + + // Nulls for explicit, "TryConvert" + _verifyToNullCoercion(MAPPER_TO_NULL, targetType); + _verifyToNullCoercion(MAPPER_TRY_CONVERT, targetType); + } + + // But as-empty needs separate + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, Boolean.class, Boolean.FALSE); + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, Character.class, Character.valueOf('\0')); + + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, Byte.class, Byte.valueOf((byte) 0)); + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, Short.class, Short.valueOf((short) 0)); + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, Integer.class, Integer.valueOf(0)); + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, Long.class, Long.valueOf(0L)); + + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, Float.class, Float.valueOf(0f)); + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, Double.class, Double.valueOf(0d)); + + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, BigInteger.class, BigInteger.ZERO); + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, BigDecimal.class, new BigDecimal(BigInteger.ZERO)); + } + + public void testOtherScalarsFromEmptyArray() throws Exception + { + for (Class<?> targetType : new Class<?>[] { + String.class, StringBuilder.class, + UUID.class, URL.class, URI.class, + Date.class, Calendar.class + }) { + _verifyFailForEmptyArray(DEFAULT_READER, targetType); + _verifyFailForEmptyArray(MAPPER_TO_FAIL, targetType); + + // Nulls for explicit, "TryConvert" + _verifyToNullCoercion(MAPPER_TO_NULL, targetType); + _verifyToNullCoercion(MAPPER_TRY_CONVERT, targetType); + } + + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, String.class, ""); + StringBuilder sb = MAPPER_TO_EMPTY.readerFor(StringBuilder.class) + .readValue(EMPTY_ARRAY); + assertEquals(0, sb.length()); + + _verifyToEmptyCoercion(MAPPER_TO_EMPTY, UUID.class, new UUID(0L, 0L)); + } + + /* + /********************************************************** + /* Helper methods + /********************************************************** + */ + + private void _verifyToNullCoercion(ObjectMapper mapper, Class<?> cls) throws Exception { + _verifyToNullCoercion(mapper.reader(), cls); + } + + private void _verifyToNullCoercion(ObjectReader r, Class<?> cls) throws Exception { + Object result = r.forType(cls).readValue(EMPTY_ARRAY); + if (result != null) { + fail("Expect null for "+cls.getName()+", got: "+result); + } + } + + private void _verifyToEmptyCoercion(ObjectMapper mapper, Class<?> cls, Object exp) throws Exception { + _verifyToEmptyCoercion(mapper.reader(), cls, exp); + } + + private void _verifyToEmptyCoercion(ObjectReader r, Class<?> cls, Object exp) throws Exception { + Object result = r.forType(cls).readValue(EMPTY_ARRAY); + if (!exp.equals(result)) { + fail("Expect value ["+exp+"] for "+cls.getName()+", got: "+result); + } + } + + private void _verifyFailForEmptyArray(ObjectMapper mapper, Class<?> targetType) throws Exception { + _verifyFailForEmptyArray(mapper.readerFor(targetType), targetType); + } + + private void _verifyFailForEmptyArray(ObjectReader r, Class<?> targetType) throws Exception + { + try { + r.forType(targetType).readValue(EMPTY_ARRAY); + fail("Should not accept Empty Array for "+targetType.getName()+" by default"); + } catch (MismatchedInputException e) { + verifyException(e, "from Array value (token `JsonToken.START_ARRAY`)"); + } + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/convert/CoerceFloatToIntTest.java b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceFloatToIntTest.java new file mode 100644 index 000000000..5bf27e405 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceFloatToIntTest.java @@ -0,0 +1,298 @@ +package com.fasterxml.jackson.databind.convert; + +import java.math.BigInteger; + +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.MismatchedInputException; +import com.fasterxml.jackson.databind.type.LogicalType; + +public class CoerceFloatToIntTest extends BaseMapTest +{ + private final ObjectMapper DEFAULT_MAPPER = sharedMapper(); + private final ObjectReader READER_LEGACY_FAIL = DEFAULT_MAPPER.reader() + .without(DeserializationFeature.ACCEPT_FLOAT_AS_INT); + + private final ObjectMapper MAPPER_TO_EMPTY; { + MAPPER_TO_EMPTY = newJsonMapper(); + MAPPER_TO_EMPTY.coercionConfigFor(LogicalType.Integer) + .setCoercion(CoercionInputShape.Float, CoercionAction.AsEmpty); + } + + private final ObjectMapper MAPPER_TRY_CONVERT; { + MAPPER_TRY_CONVERT = newJsonMapper(); + MAPPER_TRY_CONVERT.coercionConfigFor(LogicalType.Integer) + .setCoercion(CoercionInputShape.Float, CoercionAction.TryConvert); + } + + private final ObjectMapper MAPPER_TO_NULL; { + MAPPER_TO_NULL = newJsonMapper(); + MAPPER_TO_NULL.coercionConfigFor(LogicalType.Integer) + .setCoercion(CoercionInputShape.Float, CoercionAction.AsNull); + } + + private final ObjectMapper MAPPER_TO_FAIL; { + MAPPER_TO_FAIL = newJsonMapper(); + MAPPER_TO_FAIL.coercionConfigFor(LogicalType.Integer) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail); + } + + /* + /******************************************************** + /* Test methods, defaults (legacy) + /******************************************************** + */ + + public void testLegacyDoubleToIntCoercion() throws Exception + { + // by default, should be ok + Integer I = DEFAULT_MAPPER.readValue(" 1.25 ", Integer.class); + assertEquals(1, I.intValue()); + { + IntWrapper w = DEFAULT_MAPPER.readValue("{\"i\":-2.25 }", IntWrapper.class); + assertEquals(-2, w.i); + int[] arr = DEFAULT_MAPPER.readValue("[ 1.25 ]", int[].class); + assertEquals(1, arr[0]); + } + + Long L = DEFAULT_MAPPER.readValue(" 3.33 ", Long.class); + assertEquals(3L, L.longValue()); + { + LongWrapper w = DEFAULT_MAPPER.readValue("{\"l\":-2.25 }", LongWrapper.class); + assertEquals(-2L, w.l); + long[] arr = DEFAULT_MAPPER.readValue("[ 1.25 ]", long[].class); + assertEquals(1, arr[0]); + } + + Short S = DEFAULT_MAPPER.readValue("42.33", Short.class); + assertEquals(42, S.intValue()); + + BigInteger biggie = DEFAULT_MAPPER.readValue("95.3", BigInteger.class); + assertEquals(95L, biggie.longValue()); + } + + public void testLegacyFailDoubleToInt() throws Exception + { + _verifyCoerceFail(READER_LEGACY_FAIL, Integer.class, "1.5", "java.lang.Integer"); + _verifyCoerceFail(READER_LEGACY_FAIL, Integer.TYPE, "1.5", "int"); + _verifyCoerceFail(READER_LEGACY_FAIL, IntWrapper.class, "{\"i\":-2.25 }", "int"); + _verifyCoerceFail(READER_LEGACY_FAIL, int[].class, "[ 2.5 ]", "element of `int[]`"); + } + + public void testLegacyFailDoubleToLong() throws Exception + { + _verifyCoerceFail(READER_LEGACY_FAIL, Long.class, "0.5"); + _verifyCoerceFail(READER_LEGACY_FAIL, Long.TYPE, "-2.5"); + _verifyCoerceFail(READER_LEGACY_FAIL, LongWrapper.class, "{\"l\": 7.7 }"); + _verifyCoerceFail(READER_LEGACY_FAIL, long[].class, "[ -1.35 ]", "element of `long[]`"); + } + + public void testLegacyFailDoubleToOther() throws Exception + { + _verifyCoerceFail(READER_LEGACY_FAIL, Short.class, "0.5"); + _verifyCoerceFail(READER_LEGACY_FAIL, Short.TYPE, "-2.5"); + _verifyCoerceFail(READER_LEGACY_FAIL, short[].class, "[ -1.35 ]", "element of `short[]`"); + + _verifyCoerceFail(READER_LEGACY_FAIL, Byte.class, "0.5"); + _verifyCoerceFail(READER_LEGACY_FAIL, Byte.TYPE, "-2.5"); + _verifyCoerceFail(READER_LEGACY_FAIL, byte[].class, "[ -1.35 ]", "element of `byte[]`"); + + _verifyCoerceFail(READER_LEGACY_FAIL, BigInteger.class, "25236.256"); + + // 13-Jun-2020, tatu: No explicit deserializer for `AtomicLong` yet +// _verifyCoerceFail(READER_LEGACY_FAIL, AtomicLong.class, "25236.256"); + } + + /* + /******************************************************** + /* Test methods, CoerceConfig, to null + /******************************************************** + */ + + public void testCoerceConfigFloatToNull() throws Exception + { + assertNull(MAPPER_TO_NULL.readValue("1.5", Integer.class)); + // `null` not possible for primitives, must use empty (aka default) value + assertEquals(Integer.valueOf(0), MAPPER_TO_NULL.readValue("1.5", Integer.TYPE)); + { + IntWrapper w = MAPPER_TO_NULL.readValue( "{\"i\":-2.25 }", IntWrapper.class); + assertEquals(0, w.i); + int[] ints = MAPPER_TO_NULL.readValue("[ 2.5 ]", int[].class); + assertEquals(1, ints.length); + assertEquals(0, ints[0]); + } + + assertNull(MAPPER_TO_NULL.readValue("2.5", Long.class)); + assertEquals(Long.valueOf(0L), MAPPER_TO_NULL.readValue("-4.25", Long.TYPE)); + { + LongWrapper w = MAPPER_TO_NULL.readValue( "{\"l\":-2.25 }", LongWrapper.class); + assertEquals(0L, w.l); + long[] l = MAPPER_TO_NULL.readValue("[ 2.5 ]", long[].class); + assertEquals(1, l.length); + assertEquals(0L, l[0]); + } + + assertNull(MAPPER_TO_NULL.readValue("2.5", Short.class)); + assertEquals(Short.valueOf((short) 0), MAPPER_TO_NULL.readValue("-4.25", Short.TYPE)); + { + short[] s = MAPPER_TO_NULL.readValue("[ 2.5 ]", short[].class); + assertEquals(1, s.length); + assertEquals((short) 0, s[0]); + } + + assertNull(MAPPER_TO_NULL.readValue("2.5", Byte.class)); + assertEquals(Byte.valueOf((byte) 0), MAPPER_TO_NULL.readValue("-4.25", Byte.TYPE)); + { + byte[] arr = MAPPER_TO_NULL.readValue("[ 2.5 ]", byte[].class); + assertEquals(1, arr.length); + assertEquals((byte) 0, arr[0]); + } + + assertNull(MAPPER_TO_NULL.readValue("2.5", BigInteger.class)); + { + BigInteger[] arr = MAPPER_TO_NULL.readValue("[ 2.5 ]", BigInteger[].class); + assertEquals(1, arr.length); + assertNull(arr[0]); + } + } + + /* + /******************************************************** + /* Test methods, CoerceConfig, to empty + /******************************************************** + */ + + public void testCoerceConfigFloatToEmpty() throws Exception + { + assertEquals(Integer.valueOf(0), MAPPER_TO_EMPTY.readValue("1.2", Integer.class)); + assertEquals(Integer.valueOf(0), MAPPER_TO_EMPTY.readValue("1.5", Integer.TYPE)); + { + IntWrapper w = MAPPER_TO_EMPTY.readValue( "{\"i\":-2.25 }", IntWrapper.class); + assertEquals(0, w.i); + int[] ints = MAPPER_TO_EMPTY.readValue("[ 2.5 ]", int[].class); + assertEquals(1, ints.length); + assertEquals(0, ints[0]); + } + + assertEquals(Long.valueOf(0), MAPPER_TO_EMPTY.readValue("1.2", Long.class)); + assertEquals(Long.valueOf(0), MAPPER_TO_EMPTY.readValue("1.5", Long.TYPE)); + { + LongWrapper w = MAPPER_TO_EMPTY.readValue( "{\"l\":-2.25 }", LongWrapper.class); + assertEquals(0L, w.l); + long[] l = MAPPER_TO_EMPTY.readValue("[ 2.5 ]", long[].class); + assertEquals(1, l.length); + assertEquals(0L, l[0]); + } + + assertEquals(Short.valueOf((short)0), MAPPER_TO_EMPTY.readValue("1.2", Short.class)); + assertEquals(Short.valueOf((short) 0), MAPPER_TO_EMPTY.readValue("1.5", Short.TYPE)); + + assertEquals(Byte.valueOf((byte)0), MAPPER_TO_EMPTY.readValue("1.2", Byte.class)); + assertEquals(Byte.valueOf((byte) 0), MAPPER_TO_EMPTY.readValue("1.5", Byte.TYPE)); + + assertEquals(BigInteger.valueOf(0L), MAPPER_TO_EMPTY.readValue("124.5", BigInteger.class)); + } + + /* + /******************************************************** + /* Test methods, CoerceConfig, coerce + /******************************************************** + */ + + public void testCoerceConfigFloatSuccess() throws Exception + { + assertEquals(Integer.valueOf(1), MAPPER_TRY_CONVERT.readValue("1.2", Integer.class)); + assertEquals(Integer.valueOf(3), MAPPER_TRY_CONVERT.readValue("3.4", Integer.TYPE)); + { + IntWrapper w = MAPPER_TRY_CONVERT.readValue( "{\"i\":-2.25 }", IntWrapper.class); + assertEquals(-2, w.i); + int[] ints = MAPPER_TRY_CONVERT.readValue("[ 22.10 ]", int[].class); + assertEquals(1, ints.length); + assertEquals(22, ints[0]); + } + + assertEquals(Long.valueOf(1), MAPPER_TRY_CONVERT.readValue("1.2", Long.class)); + assertEquals(Long.valueOf(1), MAPPER_TRY_CONVERT.readValue("1.5", Long.TYPE)); + { + LongWrapper w = MAPPER_TRY_CONVERT.readValue( "{\"l\":-2.25 }", LongWrapper.class); + assertEquals(-2L, w.l); + long[] l = MAPPER_TRY_CONVERT.readValue("[ 2.2 ]", long[].class); + assertEquals(1, l.length); + assertEquals(2L, l[0]); + } + + assertEquals(Short.valueOf((short)1), MAPPER_TRY_CONVERT.readValue("1.2", Short.class)); + assertEquals(Short.valueOf((short) 19), MAPPER_TRY_CONVERT.readValue("19.2", Short.TYPE)); + + assertEquals(Byte.valueOf((byte)1), MAPPER_TRY_CONVERT.readValue("1.2", Byte.class)); + assertEquals(Byte.valueOf((byte) 1), MAPPER_TRY_CONVERT.readValue("1.5", Byte.TYPE)); + + assertEquals(BigInteger.valueOf(124L), MAPPER_TRY_CONVERT.readValue("124.2", BigInteger.class)); + } + + /* + /******************************************************** + /* Test methods, CoerceConfig, fail + /******************************************************** + */ + + public void testCoerceConfigFailFromFloat() throws Exception + { + _verifyCoerceFail(MAPPER_TO_FAIL, Integer.class, "1.5"); + _verifyCoerceFail(MAPPER_TO_FAIL, Integer.TYPE, "1.5"); + _verifyCoerceFail(MAPPER_TO_FAIL, IntWrapper.class, "{\"i\":-2.25 }", "int"); + _verifyCoerceFail(MAPPER_TO_FAIL, int[].class, "[ 2.5 ]", "element of `int[]`"); + + _verifyCoerceFail(MAPPER_TO_FAIL, Long.class, "0.5"); + _verifyCoerceFail(MAPPER_TO_FAIL, Long.TYPE, "-2.5"); + _verifyCoerceFail(MAPPER_TO_FAIL, LongWrapper.class, "{\"l\": 7.7 }"); + _verifyCoerceFail(MAPPER_TO_FAIL, long[].class, "[ -1.35 ]", "element of `long[]`"); + + _verifyCoerceFail(MAPPER_TO_FAIL, Short.class, "0.5"); + _verifyCoerceFail(MAPPER_TO_FAIL, Short.TYPE, "-2.5"); + _verifyCoerceFail(MAPPER_TO_FAIL, short[].class, "[ -1.35 ]", "element of `short[]`"); + + _verifyCoerceFail(MAPPER_TO_FAIL, Byte.class, "0.5"); + _verifyCoerceFail(MAPPER_TO_FAIL, Byte.TYPE, "-2.5"); + _verifyCoerceFail(MAPPER_TO_FAIL, byte[].class, "[ -1.35 ]", "element of `byte[]`"); + + _verifyCoerceFail(MAPPER_TO_FAIL, BigInteger.class, "25236.256"); + } + + /* + /******************************************************** + /* Helper methods + /******************************************************** + */ + + private void _verifyCoerceFail(ObjectMapper m, Class<?> targetType, + String doc) throws Exception + { + _verifyCoerceFail(m.reader(), targetType, doc, targetType.getName()); + } + + private void _verifyCoerceFail(ObjectMapper m, Class<?> targetType, + String doc, String targetTypeDesc) throws Exception + { + _verifyCoerceFail(m.reader(), targetType, doc, targetTypeDesc); + } + + private void _verifyCoerceFail(ObjectReader r, Class<?> targetType, + String doc) throws Exception + { + _verifyCoerceFail(r, targetType, doc, targetType.getName()); + } + + private void _verifyCoerceFail(ObjectReader r, Class<?> targetType, + String doc, String targetTypeDesc) throws Exception + { + try { + r.forType(targetType).readValue(doc); + fail("Should not accept Float for "+targetType.getName()+" by default"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot coerce Floating-point"); + verifyException(e, targetTypeDesc); + } + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/struct/ScalarCoercionTest.java b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceJDKScalarsTest.java index 0a20c7626..0fbade77e 100644 --- a/src/test/java/com/fasterxml/jackson/databind/struct/ScalarCoercionTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceJDKScalarsTest.java @@ -1,20 +1,22 @@ -package com.fasterxml.jackson.databind.struct; +package com.fasterxml.jackson.databind.convert; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringReader; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.concurrent.atomic.AtomicBoolean; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.exc.MismatchedInputException; -public class ScalarCoercionTest extends BaseMapTest +// Tests for "old" coercions (pre-2.12), with `MapperFeature.ALLOW_COERCION_OF_SCALARS` +public class CoerceJDKScalarsTest extends BaseMapTest { static class BooleanPOJO { - public boolean value; + public Boolean value; } private final ObjectMapper COERCING_MAPPER = jsonMapperBuilder() @@ -55,6 +57,7 @@ public class ScalarCoercionTest extends BaseMapTest _verifyNullOkFromEmpty(BigInteger.class, null); _verifyNullOkFromEmpty(BigDecimal.class, null); + _verifyNullOkFromEmpty(AtomicBoolean.class, null); } private void _verifyNullOkFromEmpty(Class<?> type, Object exp) throws IOException @@ -91,6 +94,7 @@ public class ScalarCoercionTest extends BaseMapTest _verifyNullFail(BigInteger.class); _verifyNullFail(BigDecimal.class); + _verifyNullFail(AtomicBoolean.class); } private void _verifyNullFail(Class<?> type) throws IOException @@ -100,7 +104,6 @@ public class ScalarCoercionTest extends BaseMapTest fail("Should have failed for "+type); } catch (MismatchedInputException e) { verifyException(e, "Cannot coerce empty String"); - verifyException(e, "Null value for"); } } @@ -110,7 +113,7 @@ public class ScalarCoercionTest extends BaseMapTest /********************************************************** */ - public void testStringCoercionOk() throws Exception + public void testStringCoercionOkBoolean() throws Exception { // first successful coercions. Boolean has a ton... _verifyCoerceSuccess("1", Boolean.TYPE, Boolean.TRUE); @@ -119,13 +122,20 @@ public class ScalarCoercionTest extends BaseMapTest _verifyCoerceSuccess(quote("true"), Boolean.class, Boolean.TRUE); _verifyCoerceSuccess(quote("True"), Boolean.TYPE, Boolean.TRUE); _verifyCoerceSuccess(quote("True"), Boolean.class, Boolean.TRUE); + _verifyCoerceSuccess(quote("TRUE"), Boolean.TYPE, Boolean.TRUE); + _verifyCoerceSuccess(quote("TRUE"), Boolean.class, Boolean.TRUE); _verifyCoerceSuccess("0", Boolean.TYPE, Boolean.FALSE); _verifyCoerceSuccess("0", Boolean.class, Boolean.FALSE); _verifyCoerceSuccess(quote("false"), Boolean.TYPE, Boolean.FALSE); _verifyCoerceSuccess(quote("false"), Boolean.class, Boolean.FALSE); _verifyCoerceSuccess(quote("False"), Boolean.TYPE, Boolean.FALSE); _verifyCoerceSuccess(quote("False"), Boolean.class, Boolean.FALSE); + _verifyCoerceSuccess(quote("FALSE"), Boolean.TYPE, Boolean.FALSE); + _verifyCoerceSuccess(quote("FALSE"), Boolean.class, Boolean.FALSE); + } + public void testStringCoercionOkNumbers() throws Exception + { _verifyCoerceSuccess(quote("123"), Byte.TYPE, Byte.valueOf((byte) 123)); _verifyCoerceSuccess(quote("123"), Byte.class, Byte.valueOf((byte) 123)); _verifyCoerceSuccess(quote("123"), Short.TYPE, Short.valueOf((short) 123)); @@ -141,12 +151,27 @@ public class ScalarCoercionTest extends BaseMapTest _verifyCoerceSuccess(quote("123"), BigInteger.class, BigInteger.valueOf(123)); _verifyCoerceSuccess(quote("123.0"), BigDecimal.class, new BigDecimal("123.0")); + + AtomicBoolean ab = COERCING_MAPPER.readValue(quote("true"), AtomicBoolean.class); + assertNotNull(ab); + assertTrue(ab.get()); } - public void testStringCoercionFail() throws Exception + public void testStringCoercionFailBoolean() throws Exception { _verifyRootStringCoerceFail("true", Boolean.TYPE); _verifyRootStringCoerceFail("true", Boolean.class); + _verifyRootStringCoerceFail("True", Boolean.TYPE); + _verifyRootStringCoerceFail("True", Boolean.class); + _verifyRootStringCoerceFail("TRUE", Boolean.TYPE); + _verifyRootStringCoerceFail("TRUE", Boolean.class); + + _verifyRootStringCoerceFail("false", Boolean.TYPE); + _verifyRootStringCoerceFail("false", Boolean.class); + } + + public void testStringCoercionFailInteger() throws Exception + { _verifyRootStringCoerceFail("123", Byte.TYPE); _verifyRootStringCoerceFail("123", Byte.class); _verifyRootStringCoerceFail("123", Short.TYPE); @@ -155,6 +180,10 @@ public class ScalarCoercionTest extends BaseMapTest _verifyRootStringCoerceFail("123", Integer.class); _verifyRootStringCoerceFail("123", Long.TYPE); _verifyRootStringCoerceFail("123", Long.class); + } + + public void testStringCoercionFailFloat() throws Exception + { _verifyRootStringCoerceFail("123.5", Float.TYPE); _verifyRootStringCoerceFail("123.5", Float.class); _verifyRootStringCoerceFail("123.5", Double.TYPE); @@ -192,8 +221,10 @@ public class ScalarCoercionTest extends BaseMapTest { // And then we have coercions from more esoteric types too - _verifyCoerceFail("65", Character.class); - _verifyCoerceFail("65", Character.TYPE); + _verifyCoerceFail("65", Character.class, + "Cannot coerce Integer value (65) to `java.lang.Character` value"); + _verifyCoerceFail("65", Character.TYPE, + "Cannot coerce Integer value (65) to `char` value"); } /* @@ -209,16 +240,15 @@ public class ScalarCoercionTest extends BaseMapTest assertEquals(exp, result); } - private void _verifyCoerceFail(String input, Class<?> type) throws IOException + private void _verifyCoerceFail(String input, Class<?> type, + String... expMatches) throws IOException { try { NOT_COERCING_MAPPER.readerFor(type) .readValue(input); fail("Should not have allowed coercion"); } catch (MismatchedInputException e) { - verifyException(e, "Cannot coerce "); - verifyException(e, " for type `"); - verifyException(e, "enable `MapperFeature.ALLOW_COERCION_OF_SCALARS` to allow"); + verifyException(e, expMatches); } } @@ -245,8 +275,8 @@ public class ScalarCoercionTest extends BaseMapTest fail("Should not have allowed coercion"); } catch (MismatchedInputException e) { verifyException(e, "Cannot coerce "); - verifyException(e, " for type `"); - verifyException(e, "enable `MapperFeature.ALLOW_COERCION_OF_SCALARS` to allow"); + verifyException(e, " to `"); + verifyException(e, "` value"); assertNotNull(e.getProcessor()); assertSame(p, e.getProcessor()); @@ -282,7 +312,7 @@ public class ScalarCoercionTest extends BaseMapTest JsonToken tokenType, String tokenValue) throws IOException { // 2 different possibilities here - verifyException(e, "Cannot coerce Number", "Cannot deserialize instance of `"); + verifyException(e, "Cannot coerce Integer value", "Cannot deserialize value of type `"); JsonParser p = (JsonParser) e.getProcessor(); diff --git a/src/test/java/com/fasterxml/jackson/databind/convert/CoerceMiscScalarsTest.java b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceMiscScalarsTest.java new file mode 100644 index 000000000..405dd35e3 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceMiscScalarsTest.java @@ -0,0 +1,281 @@ +package com.fasterxml.jackson.databind.convert; + +import java.io.File; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.Calendar; +import java.util.Currency; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; +import java.util.UUID; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.cfg.CoercionAction; +import com.fasterxml.jackson.databind.cfg.CoercionInputShape; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; + +public class CoerceMiscScalarsTest extends BaseMapTest +{ + private final ObjectMapper DEFAULT_MAPPER = sharedMapper(); + + private final ObjectMapper MAPPER_EMPTY_TO_EMPTY; + { + MAPPER_EMPTY_TO_EMPTY = newJsonMapper(); + MAPPER_EMPTY_TO_EMPTY.coercionConfigDefaults() + .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty); + } + + private final ObjectMapper MAPPER_EMPTY_TO_TRY_CONVERT; + { + MAPPER_EMPTY_TO_TRY_CONVERT = newJsonMapper(); + MAPPER_EMPTY_TO_TRY_CONVERT.coercionConfigDefaults() + .setCoercion(CoercionInputShape.EmptyString, CoercionAction.TryConvert); + } + + private final ObjectMapper MAPPER_EMPTY_TO_NULL; + { + MAPPER_EMPTY_TO_NULL = newJsonMapper(); + MAPPER_EMPTY_TO_NULL.coercionConfigDefaults() + .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); + } + + private final ObjectMapper MAPPER_EMPTY_TO_FAIL; + { + MAPPER_EMPTY_TO_FAIL = newJsonMapper(); + MAPPER_EMPTY_TO_FAIL.coercionConfigDefaults() + .setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail); + } + + private final String JSON_EMPTY = quote(""); + + /* + /******************************************************** + /* Test methods, defaults (legacy) + /******************************************************** + */ + + public void testScalarDefaultsFromEmpty() throws Exception + { + // mostly as null, with some exceptions + + _testScalarEmptyToNull(DEFAULT_MAPPER, File.class); + _testScalarEmptyToNull(DEFAULT_MAPPER, URL.class); + + _testScalarEmptyToEmpty(DEFAULT_MAPPER, URI.class, + URI.create("")); + + _testScalarEmptyToNull(DEFAULT_MAPPER, Class.class); + _testScalarEmptyToNull(DEFAULT_MAPPER, JavaType.class); + _testScalarEmptyToNull(DEFAULT_MAPPER, Currency.class); + _testScalarEmptyToNull(DEFAULT_MAPPER, Pattern.class); + + _testScalarEmptyToEmpty(DEFAULT_MAPPER, Locale.class, + Locale.ROOT); + + _testScalarEmptyToNull(DEFAULT_MAPPER, Charset.class); + _testScalarEmptyToNull(DEFAULT_MAPPER, TimeZone.class); + _testScalarEmptyToNull(DEFAULT_MAPPER, InetAddress.class); + _testScalarEmptyToNull(DEFAULT_MAPPER, InetSocketAddress.class); + } + + /* + /******************************************************** + /* Test methods, successful coercions from empty String + /******************************************************** + */ + + public void testScalarEmptyToNull() throws Exception + { + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, File.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, URL.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, URI.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, Class.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, JavaType.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, Currency.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, Pattern.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, Locale.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, Charset.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, TimeZone.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, InetAddress.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, InetSocketAddress.class); + } + + public void testScalarEmptyToEmpty() throws Exception + { + _testScalarEmptyToNull(MAPPER_EMPTY_TO_EMPTY, File.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_EMPTY, URL.class); + + _testScalarEmptyToEmpty(MAPPER_EMPTY_TO_EMPTY, URI.class, + URI.create("")); + + _testScalarEmptyToNull(MAPPER_EMPTY_TO_EMPTY, Class.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_EMPTY, JavaType.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_EMPTY, Currency.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_EMPTY, Pattern.class); + + _testScalarEmptyToEmpty(MAPPER_EMPTY_TO_EMPTY, Locale.class, + Locale.ROOT); + + _testScalarEmptyToNull(MAPPER_EMPTY_TO_EMPTY, Charset.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_EMPTY, TimeZone.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_EMPTY, InetAddress.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_EMPTY, InetSocketAddress.class); + } + + public void testScalarEmptyToTryConvert() throws Exception + { + // Should be same as `AsNull` for most but not all + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, File.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, URL.class); + + _testScalarEmptyToEmpty(MAPPER_EMPTY_TO_TRY_CONVERT, URI.class, + URI.create("")); + + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, Class.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, JavaType.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, Currency.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, Pattern.class); + + _testScalarEmptyToEmpty(MAPPER_EMPTY_TO_TRY_CONVERT, Locale.class, + Locale.ROOT); + + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, Charset.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, TimeZone.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, InetAddress.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, InetSocketAddress.class); + } + + /* + /******************************************************** + /* Test methods, failed coercions from empty String + /******************************************************** + */ + + public void testScalarsFailFromEmpty() throws Exception + { + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, File.class); + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, URL.class); + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, URI.class); + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, Class.class); + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, JavaType.class); + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, Currency.class); + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, Pattern.class); + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, Locale.class); + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, Charset.class); + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, TimeZone.class); + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, InetAddress.class); + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, InetSocketAddress.class); + } + + /* + /******************************************************** + /* Test methods, (more) special type(s) + /******************************************************** + */ + + // UUID is quite compatible, but not exactly due to historical reasons; + // also uses custom subtype, so test separately + + public void testUUIDCoercions() throws Exception + { + // Coerce to `null` both by default, "TryConvert" and explicit + _testScalarEmptyToNull(DEFAULT_MAPPER, UUID.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, UUID.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, UUID.class); + + // but allow separate "empty" value is specifically requested + _testScalarEmptyToEmpty(MAPPER_EMPTY_TO_EMPTY, UUID.class, + new UUID(0L, 0L)); + + // allow forcing failure, too + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, UUID.class); + + // and allow failure with specifically configured per-class override, too + ObjectMapper failMapper = newJsonMapper(); + failMapper.coercionConfigFor(UUID.class) + .setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail); + _verifyScalarToFail(failMapper, UUID.class); + } + + // StringBuilder is its own special type, since it naturally maps + // from String values, hence separate testing + public void testStringBuilderCoercions() throws Exception + { + // should result in an "empty" StringBuilder for all valid settings + _checkEmptyStringBuilder(DEFAULT_MAPPER.readValue(JSON_EMPTY, StringBuilder.class)); + _checkEmptyStringBuilder(MAPPER_EMPTY_TO_EMPTY.readValue(JSON_EMPTY, StringBuilder.class)); + _checkEmptyStringBuilder(MAPPER_EMPTY_TO_TRY_CONVERT.readValue(JSON_EMPTY, StringBuilder.class)); + _checkEmptyStringBuilder(MAPPER_EMPTY_TO_NULL.readValue(JSON_EMPTY, StringBuilder.class)); + // and even alleged failure should not result in that since it's not coercion + _checkEmptyStringBuilder(MAPPER_EMPTY_TO_FAIL.readValue(JSON_EMPTY, StringBuilder.class)); + } + + private void _checkEmptyStringBuilder(StringBuilder sb) { + assertNotNull(sb); + assertEquals(0, sb.length()); + } + + // Date, Calendar also included here for convenience + + public void testLegacyDateTimeCoercions() throws Exception + { + // Coerce to `null` both by default, "TryConvert" and explicit + _testScalarEmptyToNull(DEFAULT_MAPPER, Calendar.class); + _testScalarEmptyToNull(DEFAULT_MAPPER, Date.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, Calendar.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_NULL, Date.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, Calendar.class); + _testScalarEmptyToNull(MAPPER_EMPTY_TO_TRY_CONVERT, Date.class); + + // but allow separate "empty" value is specifically requested + Calendar emptyCal = new GregorianCalendar(); + emptyCal.setTimeInMillis(0L); +// _testScalarEmptyToEmpty(MAPPER_EMPTY_TO_EMPTY, Calendar.class, emptyCal); + _testScalarEmptyToEmpty(MAPPER_EMPTY_TO_EMPTY, Date.class, new Date(0L)); + + // allow forcing failure, too + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, Calendar.class); + _verifyScalarToFail(MAPPER_EMPTY_TO_FAIL, Date.class); + } + + /* + /******************************************************** + /* Second-level test helper methods + /******************************************************** + */ + + private void _testScalarEmptyToNull(ObjectMapper mapper, Class<?> target) throws Exception + { + assertNull(mapper.readerFor(target).readValue(JSON_EMPTY)); + } + + private void _testScalarEmptyToEmpty(ObjectMapper mapper, + Class<?> target, Object emptyValue) throws Exception + { + Object result = mapper.readerFor(target).readValue(JSON_EMPTY); + if (result == null) { + fail("Expected empty, non-null value for "+target.getName()+", got null"); + } + assertEquals(emptyValue, result); + } + + private void _verifyScalarToFail(ObjectMapper mapper, Class<?> target) throws Exception + { + try { + /*Object result =*/ mapper.readerFor(target) + .readValue(JSON_EMPTY); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot coerce empty String "); + verifyException(e, " to `"+target.getName()); + } + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/convert/CoercePojosTest.java b/src/test/java/com/fasterxml/jackson/databind/convert/CoercePojosTest.java new file mode 100644 index 000000000..79d63c4d8 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/convert/CoercePojosTest.java @@ -0,0 +1,188 @@ +package com.fasterxml.jackson.databind.convert; + +import com.fasterxml.jackson.core.JsonProcessingException; +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.MismatchedInputException; +import com.fasterxml.jackson.databind.type.LogicalType; + +public class CoercePojosTest extends BaseMapTest +{ + static class Bean { + public String a; + } + + private final ObjectMapper MAPPER = newJsonMapper(); + + private final String JSON_EMPTY = quote(""); + private final String JSON_BLANK = quote(" "); + + /* + /******************************************************** + /* Test methods, from empty String + /******************************************************** + */ + + public void testPOJOFromEmptyStringLegacy() throws Exception + { + // first, verify default settings which do not accept empty String: + assertFalse(MAPPER.isEnabled(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)); + + // should be ok to enable dynamically + _verifyFromEmptyPass(MAPPER.reader() + .with(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT), + JSON_EMPTY); + + } + + public void testPOJOFromEmptyGlobalConfig() throws Exception + { + _testPOJOFromEmptyGlobalConfig(CoercionInputShape.EmptyString, JSON_EMPTY, null); + } + + public void testPOJOFromEmptyLogicalTypeConfig() throws Exception + { + _testPOJOFromEmptyLogicalTypeConfig(CoercionInputShape.EmptyString, JSON_EMPTY, null); + } + + public void testPOJOFromEmptyPhysicalTypeConfig() throws Exception + { + _testPOJOFromEmptyPhysicalTypeConfig(CoercionInputShape.EmptyString, JSON_EMPTY, null); + } + + /* + /******************************************************** + /* Test methods, from blank String + /******************************************************** + */ + + public void testPOJOFromBlankGlobalConfig() throws Exception + { + _testPOJOFromEmptyGlobalConfig(CoercionInputShape.EmptyString, JSON_BLANK, Boolean.TRUE); + } + + public void testPOJOFromBlankLogicalTypeConfig() throws Exception + { + _testPOJOFromEmptyLogicalTypeConfig(CoercionInputShape.EmptyString, JSON_BLANK, Boolean.TRUE); + } + + public void testPOJOFromBlankPhysicalTypeConfig() throws Exception + { + _testPOJOFromEmptyPhysicalTypeConfig(CoercionInputShape.EmptyString, JSON_BLANK, Boolean.TRUE); + } + + /* + /******************************************************** + /* Second-level helper methods + /******************************************************** + */ + + private void _testPOJOFromEmptyGlobalConfig(final CoercionInputShape shape, final String json, + Boolean allowEmpty) + throws Exception + { + ObjectMapper mapper; + + // First, coerce to null + mapper = newJsonMapper(); + mapper.coercionConfigDefaults().setCoercion(shape, CoercionAction.AsNull) + .setAcceptBlankAsEmpty(allowEmpty); + assertNull(_verifyFromEmptyPass(mapper, json)); + + // Then coerce as empty + mapper = newJsonMapper(); + mapper.coercionConfigDefaults().setCoercion(shape, CoercionAction.AsEmpty) + .setAcceptBlankAsEmpty(allowEmpty); + Bean b = _verifyFromEmptyPass(mapper, json); + assertNotNull(b); + + // and finally, "try convert", which aliases to 'null' + mapper = newJsonMapper(); + mapper.coercionConfigDefaults().setCoercion(shape, CoercionAction.TryConvert) + .setAcceptBlankAsEmpty(allowEmpty); + assertNull(_verifyFromEmptyPass(mapper, json)); + } + + private void _testPOJOFromEmptyLogicalTypeConfig(final CoercionInputShape shape, final String json, + Boolean allowEmpty) + throws Exception + { + ObjectMapper mapper; + + // First, coerce to null + mapper = newJsonMapper(); + mapper.coercionConfigFor(LogicalType.POJO).setCoercion(shape, CoercionAction.AsNull) + .setAcceptBlankAsEmpty(allowEmpty); + assertNull(_verifyFromEmptyPass(mapper, json)); + + // Then coerce as empty + mapper = newJsonMapper(); + mapper.coercionConfigFor(LogicalType.POJO).setCoercion(shape, CoercionAction.AsEmpty) + .setAcceptBlankAsEmpty(allowEmpty); + Bean b = _verifyFromEmptyPass(mapper, json); + assertNotNull(b); + + // But also make fail again with 2-level settings + mapper = newJsonMapper(); + mapper.coercionConfigDefaults().setCoercion(shape, CoercionAction.AsNull) + .setAcceptBlankAsEmpty(allowEmpty); + mapper.coercionConfigFor(LogicalType.POJO).setCoercion(shape, + CoercionAction.Fail); + _verifyFromEmptyFail(mapper, json); + } + + private void _testPOJOFromEmptyPhysicalTypeConfig(final CoercionInputShape shape, final String json, + Boolean allowEmpty) + throws Exception + { + ObjectMapper mapper; + + // First, coerce to null + mapper = newJsonMapper(); + mapper.coercionConfigFor(Bean.class).setCoercion(shape, CoercionAction.AsNull) + .setAcceptBlankAsEmpty(allowEmpty); + assertNull(_verifyFromEmptyPass(mapper, json)); + + // Then coerce as empty + mapper = newJsonMapper(); + mapper.coercionConfigFor(Bean.class).setCoercion(shape, CoercionAction.AsEmpty) + .setAcceptBlankAsEmpty(allowEmpty); + Bean b = _verifyFromEmptyPass(mapper, json); + assertNotNull(b); + + // But also make fail again with 2-level settings, with physical having precedence + mapper = newJsonMapper(); + mapper.coercionConfigFor(LogicalType.POJO).setCoercion(shape, CoercionAction.AsEmpty) + .setAcceptBlankAsEmpty(allowEmpty); + mapper.coercionConfigFor(Bean.class).setCoercion(shape, CoercionAction.Fail); + _verifyFromEmptyFail(mapper, json); + } + + private Bean _verifyFromEmptyPass(ObjectMapper m, String json) throws Exception { + return _verifyFromEmptyPass(m.reader(), json); + } + + private Bean _verifyFromEmptyPass(ObjectReader r, String json) throws Exception + { + return r.forType(Bean.class) + .readValue(json); + } + + private void _verifyFromEmptyFail(ObjectMapper m, String json) throws Exception + { + try { + m.readValue(json, Bean.class); + fail("Should not accept Empty/Blank String for POJO with passed settings"); + } catch (MismatchedInputException e) { + _verifyFailMessage(e); + } + } + + private void _verifyFailMessage(JsonProcessingException e) + { + verifyException(e, "Cannot deserialize value of type "); + verifyException(e, " from empty String ", " from blank String "); + assertValidLocation(e.getLocation()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/convert/CoerceToBooleanTest.java b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceToBooleanTest.java new file mode 100644 index 000000000..f78c14db1 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/convert/CoerceToBooleanTest.java @@ -0,0 +1,273 @@ +package com.fasterxml.jackson.databind.convert; + +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.BaseMapTest; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.cfg.CoercionAction; +import com.fasterxml.jackson.databind.cfg.CoercionInputShape; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.fasterxml.jackson.databind.type.LogicalType; + +public class CoerceToBooleanTest extends BaseMapTest +{ + static class BooleanPOJO { + public boolean value; + } + + private final ObjectMapper DEFAULT_MAPPER = sharedMapper(); + + private final ObjectMapper LEGACY_NONCOERCING_MAPPER = jsonMapperBuilder() + .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS) + .build(); + + private final ObjectMapper MAPPER_TO_EMPTY; { + MAPPER_TO_EMPTY = newJsonMapper(); + MAPPER_TO_EMPTY.coercionConfigFor(LogicalType.Boolean) + .setCoercion(CoercionInputShape.Integer, CoercionAction.AsEmpty); + } + + private final ObjectMapper MAPPER_TRY_CONVERT; { + MAPPER_TRY_CONVERT = newJsonMapper(); + MAPPER_TRY_CONVERT.coercionConfigFor(LogicalType.Boolean) + .setCoercion(CoercionInputShape.Integer, CoercionAction.TryConvert); + } + + private final ObjectMapper MAPPER_TO_NULL; { + MAPPER_TO_NULL = newJsonMapper(); + MAPPER_TO_NULL.coercionConfigFor(LogicalType.Boolean) + .setCoercion(CoercionInputShape.Integer, CoercionAction.AsNull); + } + + private final ObjectMapper MAPPER_TO_FAIL; { + MAPPER_TO_FAIL = newJsonMapper(); + MAPPER_TO_FAIL.coercionConfigFor(LogicalType.Boolean) + .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail); + } + + private final static String DOC_WITH_0 = aposToQuotes("{'value':0}"); + private final static String DOC_WITH_1 = aposToQuotes("{'value':1}"); + + /* + /********************************************************** + /* Unit tests: default, legacy configuration + /********************************************************** + */ + + public void testToBooleanCoercionSuccessPojo() throws Exception + { + BooleanPOJO p; + final ObjectReader r = DEFAULT_MAPPER.readerFor(BooleanPOJO.class); + + p = r.readValue(DOC_WITH_0); + assertEquals(false, p.value); + p = r.readValue(utf8Bytes(DOC_WITH_0)); + assertEquals(false, p.value); + + p = r.readValue(DOC_WITH_1); + assertEquals(true, p.value); + p = r.readValue(utf8Bytes(DOC_WITH_1)); + assertEquals(true, p.value); + } + + public void testToBooleanCoercionSuccessRoot() throws Exception + { + final ObjectReader br = DEFAULT_MAPPER.readerFor(Boolean.class); + + assertEquals(Boolean.FALSE, br.readValue(" 0")); + assertEquals(Boolean.FALSE, br.readValue(utf8Bytes(" 0"))); + assertEquals(Boolean.TRUE, br.readValue(" -1")); + assertEquals(Boolean.TRUE, br.readValue(utf8Bytes(" -1"))); + + final ObjectReader atomicR = DEFAULT_MAPPER.readerFor(AtomicBoolean.class); + + AtomicBoolean ab; + + ab = atomicR.readValue(" 0"); + ab = atomicR.readValue(utf8Bytes(" 0")); + assertEquals(false, ab.get()); + + ab = atomicR.readValue(" 111"); + assertEquals(true, ab.get()); + ab = atomicR.readValue(utf8Bytes(" 111")); + assertEquals(true, ab.get()); + } + + public void testToBooleanCoercionFailBytes() throws Exception + { + _verifyBooleanCoerceFail(aposToQuotes("{'value':1}"), true, JsonToken.VALUE_NUMBER_INT, "1", BooleanPOJO.class); + + _verifyBooleanCoerceFail("1", true, JsonToken.VALUE_NUMBER_INT, "1", Boolean.TYPE); + _verifyBooleanCoerceFail("1", true, JsonToken.VALUE_NUMBER_INT, "1", Boolean.class); + } + + public void testToBooleanCoercionFailChars() throws Exception + { + _verifyBooleanCoerceFail(aposToQuotes("{'value':1}"), false, JsonToken.VALUE_NUMBER_INT, "1", BooleanPOJO.class); + + _verifyBooleanCoerceFail("1", false, JsonToken.VALUE_NUMBER_INT, "1", Boolean.TYPE); + _verifyBooleanCoerceFail("1", false, JsonToken.VALUE_NUMBER_INT, "1", Boolean.class); + } + + /* + /********************************************************** + /* Unit tests: new CoercionConfig, as-null, as-empty, try-coerce + /********************************************************** + */ + + public void testIntToNullCoercion() throws Exception + { + assertNull(MAPPER_TO_NULL.readValue("0", Boolean.class)); + assertNull(MAPPER_TO_NULL.readValue("1", Boolean.class)); + + // but due to coercion to `boolean`, can not return null here -- however, + // goes "1 -> false (no null for primitive) -> Boolean.FALSE + assertEquals(Boolean.FALSE, MAPPER_TO_NULL.readValue("0", Boolean.TYPE)); + assertEquals(Boolean.FALSE, MAPPER_TO_NULL.readValue("1", Boolean.TYPE)); + + // As to AtomicBoolean: that type itself IS nullable since it's of LogicalType.Boolean so + assertNull(MAPPER_TO_NULL.readValue("0", AtomicBoolean.class)); + assertNull(MAPPER_TO_NULL.readValue("1", AtomicBoolean.class)); + + BooleanPOJO p; + p = MAPPER_TO_NULL.readValue(DOC_WITH_0, BooleanPOJO.class); + assertFalse(p.value); + p = MAPPER_TO_NULL.readValue(DOC_WITH_1, BooleanPOJO.class); + assertFalse(p.value); + } + + public void testIntToEmptyCoercion() throws Exception + { + // "empty" value for Boolean/boolean is False/false + + assertEquals(Boolean.FALSE, MAPPER_TO_EMPTY.readValue("0", Boolean.class)); + assertEquals(Boolean.FALSE, MAPPER_TO_EMPTY.readValue("1", Boolean.class)); + + assertEquals(Boolean.FALSE, MAPPER_TO_EMPTY.readValue("0", Boolean.TYPE)); + assertEquals(Boolean.FALSE, MAPPER_TO_EMPTY.readValue("1", Boolean.TYPE)); + + AtomicBoolean ab; + ab = MAPPER_TO_EMPTY.readValue("0", AtomicBoolean.class); + assertFalse(ab.get()); + ab = MAPPER_TO_EMPTY.readValue("1", AtomicBoolean.class); + assertFalse(ab.get()); + + BooleanPOJO p; + p = MAPPER_TO_EMPTY.readValue(DOC_WITH_0, BooleanPOJO.class); + assertFalse(p.value); + p = MAPPER_TO_EMPTY.readValue(DOC_WITH_1, BooleanPOJO.class); + assertFalse(p.value); + } + + public void testIntToTryCoercion() throws Exception + { + // And "TryCoerce" should do what would be typically expected + + assertEquals(Boolean.FALSE, MAPPER_TRY_CONVERT.readValue("0", Boolean.class)); + assertEquals(Boolean.TRUE, MAPPER_TRY_CONVERT.readValue("1", Boolean.class)); + + assertEquals(Boolean.FALSE, MAPPER_TRY_CONVERT.readValue("0", Boolean.TYPE)); + assertEquals(Boolean.TRUE, MAPPER_TRY_CONVERT.readValue("1", Boolean.TYPE)); + + AtomicBoolean ab; + ab = MAPPER_TRY_CONVERT.readValue("0", AtomicBoolean.class); + assertFalse(ab.get()); + ab = MAPPER_TRY_CONVERT.readValue("1", AtomicBoolean.class); + assertTrue(ab.get()); + + BooleanPOJO p; + p = MAPPER_TRY_CONVERT.readValue(DOC_WITH_0, BooleanPOJO.class); + assertFalse(p.value); + p = MAPPER_TRY_CONVERT.readValue(DOC_WITH_1, BooleanPOJO.class); + assertTrue(p.value); + } + + /* + /********************************************************** + /* Unit tests: new CoercionConfig, failing + /********************************************************** + */ + + public void testFailFromInteger() throws Exception + { + _verifyFailFromInteger(MAPPER_TO_FAIL, BooleanPOJO.class, DOC_WITH_0, Boolean.TYPE); + _verifyFailFromInteger(MAPPER_TO_FAIL, BooleanPOJO.class, DOC_WITH_1, Boolean.TYPE); + + _verifyFailFromInteger(MAPPER_TO_FAIL, Boolean.class, "0"); + _verifyFailFromInteger(MAPPER_TO_FAIL, Boolean.class, "42"); + + _verifyFailFromInteger(MAPPER_TO_FAIL, Boolean.TYPE, "0"); + _verifyFailFromInteger(MAPPER_TO_FAIL, Boolean.TYPE, "999"); + + _verifyFailFromInteger(MAPPER_TO_FAIL, AtomicBoolean.class, "0"); + _verifyFailFromInteger(MAPPER_TO_FAIL, AtomicBoolean.class, "-123"); + } + + /* + /********************************************************** + /* Helper methods + /********************************************************** + */ + + private void _verifyBooleanCoerceFail(String doc, boolean useBytes, + JsonToken tokenType, String tokenValue, Class<?> targetType) throws IOException + { + // Test failure for root value: for both byte- and char-backed sources. + + // [databind#2635]: important, need to use `readValue()` that takes content and NOT + // JsonParser, as this forces closing of underlying parser and exposes more issues. + + final ObjectReader r = LEGACY_NONCOERCING_MAPPER.readerFor(targetType); + try { + if (useBytes) { + r.readValue(utf8Bytes(doc)); + } else { + r.readValue(doc); + } + fail("Should not have allowed coercion"); + } catch (MismatchedInputException e) { + _verifyBooleanCoerceFailReason(e, tokenType, tokenValue); + } + } + + @SuppressWarnings("resource") + private void _verifyBooleanCoerceFailReason(MismatchedInputException e, + JsonToken tokenType, String tokenValue) throws IOException + { + verifyException(e, "Cannot coerce "); + verifyException(e, " to `"); + + JsonParser p = (JsonParser) e.getProcessor(); + + assertToken(tokenType, p.currentToken()); + + final String text = p.getText(); + if (!tokenValue.equals(text)) { + String textDesc = (text == null) ? "NULL" : quote(text); + fail("Token text ("+textDesc+") via parser of type "+p.getClass().getName() + +" not as expected ("+quote(tokenValue)+")"); + } + } + + private void _verifyFailFromInteger(ObjectMapper m, Class<?> targetType, String doc) throws Exception { + _verifyFailFromInteger(m, targetType, doc, targetType); + } + + private void _verifyFailFromInteger(ObjectMapper m, Class<?> targetType, String doc, + Class<?> valueType) throws Exception + { + try { + m.readerFor(targetType).readValue(doc); + fail("Should not accept Integer for "+targetType.getName()+" by default"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot coerce Integer value"); + verifyException(e, "to `"+valueType.getName()+"`"); + } + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/convert/NumericConversionTest.java b/src/test/java/com/fasterxml/jackson/databind/convert/NumericConversionTest.java deleted file mode 100644 index 03f79cfe0..000000000 --- a/src/test/java/com/fasterxml/jackson/databind/convert/NumericConversionTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.fasterxml.jackson.databind.convert; - -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.exc.MismatchedInputException; - -public class NumericConversionTest extends BaseMapTest -{ - private final ObjectMapper MAPPER = sharedMapper(); - private final ObjectReader R = MAPPER.reader().without(DeserializationFeature.ACCEPT_FLOAT_AS_INT); - - public void testDoubleToInt() throws Exception - { - // by default, should be ok - Integer I = MAPPER.readValue(" 1.25 ", Integer.class); - assertEquals(1, I.intValue()); - IntWrapper w = MAPPER.readValue("{\"i\":-2.25 }", IntWrapper.class); - assertEquals(-2, w.i); - int[] arr = MAPPER.readValue("[ 1.25 ]", int[].class); - assertEquals(1, arr[0]); - - try { - R.forType(Integer.class).readValue("1.5"); - fail("Should not pass"); - } catch (JsonMappingException e) { - verifyException(e, "Cannot coerce a floating-point"); - } - try { - R.forType(Integer.TYPE).readValue("1.5"); - fail("Should not pass"); - } catch (JsonMappingException e) { - verifyException(e, "Cannot coerce a floating-point"); - } - try { - R.forType(IntWrapper.class).readValue("{\"i\":-2.25 }"); - fail("Should not pass"); - } catch (JsonMappingException e) { - verifyException(e, "Cannot coerce a floating-point"); - } - try { - R.forType(int[].class).readValue("[ 2.5 ]"); - fail("Should not pass"); - } catch (JsonMappingException e) { - verifyException(e, "Cannot coerce a floating-point"); - } - } - - public void testDoubleToLong() throws Exception - { - // by default, should be ok - Long L = MAPPER.readValue(" 3.33 ", Long.class); - assertEquals(3L, L.longValue()); - LongWrapper w = MAPPER.readValue("{\"l\":-2.25 }", LongWrapper.class); - assertEquals(-2L, w.l); - long[] arr = MAPPER.readValue("[ 1.25 ]", long[].class); - assertEquals(1, arr[0]); - - try { - R.forType(Long.class).readValue("1.5"); - fail("Should not pass"); - } catch (MismatchedInputException e) { - verifyException(e, "Cannot coerce a floating-point"); - } - - try { - R.forType(Long.TYPE).readValue("1.5"); - fail("Should not pass"); - } catch (MismatchedInputException e) { - verifyException(e, "Cannot coerce a floating-point"); - } - - try { - R.forType(LongWrapper.class).readValue("{\"l\": 7.7 }"); - fail("Should not pass"); - } catch (MismatchedInputException e) { - verifyException(e, "Cannot coerce a floating-point"); - } - try { - R.forType(long[].class).readValue("[ 2.5 ]"); - fail("Should not pass"); - } catch (MismatchedInputException e) { - verifyException(e, "Cannot coerce a floating-point"); - } - } -} diff --git a/src/test/java/com/fasterxml/jackson/databind/convert/TestArrayConversions.java b/src/test/java/com/fasterxml/jackson/databind/convert/TestArrayConversions.java index fbc4751bb..33117d4aa 100644 --- a/src/test/java/com/fasterxml/jackson/databind/convert/TestArrayConversions.java +++ b/src/test/java/com/fasterxml/jackson/databind/convert/TestArrayConversions.java @@ -12,7 +12,7 @@ public class TestArrayConversions extends com.fasterxml.jackson.databind.BaseMapTest { final static String OVERFLOW_MSG_BYTE = "out of range of Java byte"; - final static String OVERFLOW_MSG = "overflow"; + final static String OVERFLOW_MSG_SHORT = "out of range of Java short"; final static String OVERFLOW_MSG_INT = "out of range of int"; final static String OVERFLOW_MSG_LONG = "out of range of long"; @@ -103,7 +103,7 @@ public class TestArrayConversions try { MAPPER.convertValue(new int[] { -99999 }, short[].class); } catch (IllegalArgumentException e) { - verifyException(e, OVERFLOW_MSG); + verifyException(e, OVERFLOW_MSG_SHORT); } // Int overflow try { diff --git a/src/test/java/com/fasterxml/jackson/databind/convert/TestMapConversions.java b/src/test/java/com/fasterxml/jackson/databind/convert/TestMapConversions.java index bfed9dffe..aabe0eaa3 100644 --- a/src/test/java/com/fasterxml/jackson/databind/convert/TestMapConversions.java +++ b/src/test/java/com/fasterxml/jackson/databind/convert/TestMapConversions.java @@ -70,11 +70,11 @@ public class TestMapConversions public void testMapToBean() { EnumMap<AB,String> map = new EnumMap<AB,String>(AB.class); - map.put(AB.A, " 17"); - map.put(AB.B, " -1"); + map.put(AB.A, "17"); + map.put(AB.B, "-1"); Bean bean = MAPPER.convertValue(map, Bean.class); assertEquals(Integer.valueOf(17), bean.A); - assertEquals(" -1", bean.B); + assertEquals("-1", bean.B); } public void testBeanToMap() diff --git a/src/test/java/com/fasterxml/jackson/databind/convert/TestStringConversions.java b/src/test/java/com/fasterxml/jackson/databind/convert/TestStringConversions.java index 93a9d67b1..5f874ce93 100644 --- a/src/test/java/com/fasterxml/jackson/databind/convert/TestStringConversions.java +++ b/src/test/java/com/fasterxml/jackson/databind/convert/TestStringConversions.java @@ -35,7 +35,7 @@ public class TestStringConversions public void testSimple() { assertEquals(Boolean.TRUE, MAPPER.convertValue("true", Boolean.class)); - assertEquals(Integer.valueOf(-3), MAPPER.convertValue(" -3 ", Integer.class)); + assertEquals(Integer.valueOf(-3), MAPPER.convertValue("-3", Integer.class)); assertEquals(Long.valueOf(77), MAPPER.convertValue("77", Long.class)); int[] ints = { 1, 2, 3 }; diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/ReadOnlyDeser1805Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/ReadOnlyDeser1805Test.java deleted file mode 100644 index 7741337eb..000000000 --- a/src/test/java/com/fasterxml/jackson/databind/deser/ReadOnlyDeser1805Test.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.fasterxml.jackson.databind.deser; - -import java.util.*; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.*; - -public class ReadOnlyDeser1805Test extends BaseMapTest -{ - static class Foo { - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - private List<Long> list = new ArrayList<>(); - - List<Long> getList() { - return list; - } - - public Foo setList(List<Long> list) { - this.list = list; - return this; - } - } - - // [databind#1805] - static class UserWithReadOnly { - public String name; - - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - public List<String> getRoles() { - return Arrays.asList("admin", "monitor"); - } - } - - // [databind#1805] - @JsonIgnoreProperties(value={ "roles" }, allowGetters=true) - static class UserAllowGetters { - public String name; - - public List<String> getRoles() { - return Arrays.asList("admin", "monitor"); - } - } - - /* - /********************************************************** - /* Test methods - /********************************************************** - */ - - private final ObjectMapper MAPPER = newJsonMapper(); - - public void testReadOnly1382() throws Exception - { - String payload = "{\"list\":[1,2,3,4]}"; - Foo foo = MAPPER.readValue(payload, Foo.class); - assertTrue("List should be empty", foo.getList().isEmpty()); - } - - // [databind#1805] - public void testViaReadOnly() throws Exception { - UserWithReadOnly user = new UserWithReadOnly(); - user.name = "foo"; - String json = MAPPER.writeValueAsString(user); - UserWithReadOnly result = MAPPER.readValue(json, UserWithReadOnly.class); - assertNotNull(result); - } - - // [databind#1805] - public void testUsingAllowGetters() throws Exception { - UserAllowGetters user = new UserAllowGetters(); - user.name = "foo"; - String json = MAPPER.writeValueAsString(user); - assertTrue(json.contains("roles")); - UserAllowGetters result = MAPPER.readValue(json, UserAllowGetters.class); - assertNotNull(result); - } -} diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/ReadOnlyDeserFailOnUnknown2719Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/ReadOnlyDeserFailOnUnknown2719Test.java new file mode 100644 index 000000000..7e1f6ea51 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/ReadOnlyDeserFailOnUnknown2719Test.java @@ -0,0 +1,52 @@ +package com.fasterxml.jackson.databind.deser; + +import com.fasterxml.jackson.annotation.*; + +import com.fasterxml.jackson.databind.*; + +public class ReadOnlyDeserFailOnUnknown2719Test extends BaseMapTest +{ + // [databind#2719] + static class UserWithReadOnly { + @JsonProperty(value = "username", access = JsonProperty.Access.READ_ONLY) + public String name; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String password; + public String login; + } + + /* + /********************************************************** + /* Test methods + /********************************************************** + */ + + private final ObjectMapper MAPPER = newJsonMapper(); + + public void testFailOnIgnore() throws Exception + { + ObjectReader r = MAPPER.readerFor(UserWithReadOnly.class); + + // First, fine to get 'login' + UserWithReadOnly result = r.readValue(aposToQuotes("{'login':'foo'}")); + assertEquals("foo", result.login); + + // but not 'password' + r = r.with(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES); + try { + r.readValue(aposToQuotes("{'login':'foo', 'password':'bar'}")); + fail("Should fail"); + } catch (JsonMappingException e) { + verifyException(e, "Ignored field"); + } + + // or 'username' + r = r.with(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES); + try { + r.readValue(aposToQuotes("{'login':'foo', 'username':'bar'}")); + fail("Should fail"); + } catch (JsonMappingException e) { + verifyException(e, "Ignored field"); + } + } +} diff --git a/src/test/java/com/fasterxml/jackson/failing/PropertyAccessReadOnly2118Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/ReadOnlyListDeser2118Test.java index fa7860885..0fb1e5cb9 100644 --- a/src/test/java/com/fasterxml/jackson/failing/PropertyAccessReadOnly2118Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/ReadOnlyListDeser2118Test.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.failing; +package com.fasterxml.jackson.databind.deser; import java.util.*; @@ -6,12 +6,11 @@ import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.*; -public class PropertyAccessReadOnly2118Test extends BaseMapTest +public class ReadOnlyListDeser2118Test extends BaseMapTest { // [databind#2118] static class SecurityGroup { - - private List<SecurityGroupRule> securityGroupRules; + List<SecurityGroupRule> securityGroupRules; public SecurityGroup() { this.securityGroupRules = new ArrayList<>(); @@ -40,15 +39,21 @@ public class PropertyAccessReadOnly2118Test extends BaseMapTest public void setId(String id) { this.id = id; } + + @Override + public String toString() { + return "{SecurityGroupRule '"+id+"'}"; + } } + private final ObjectMapper mapper = newJsonMapper(); + // [databind#2118] public void testAccessReadOnly() throws Exception { - String data ="{\"security_group_rules\": [{\"id\": \"id1\"}, {\"id\": \"id2\"}, {\"id\": \"id3\"}, {\"id\": \"id4\"}]}"; - ObjectMapper mapper = new ObjectMapper(); -// This would work around the issue: + String data ="{\"security_group_rules\": [{\"id\": \"id1\"}]}"; +// This would work around the issue: // mapper.disable(MapperFeature.USE_GETTERS_AS_SETTERS); SecurityGroup sg = mapper.readValue(data, SecurityGroup.class); - System.out.println(mapper.writeValueAsString(sg)); + assertEquals(Collections.emptyList(), sg.securityGroupRules); } } diff --git a/src/test/java/com/fasterxml/jackson/failing/ReadOnlyList2283Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/ReadOnlyListDeser2283Test.java index 533265fc4..66e0fc527 100644 --- a/src/test/java/com/fasterxml/jackson/failing/ReadOnlyList2283Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/ReadOnlyListDeser2283Test.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.failing; +package com.fasterxml.jackson.databind.deser; import java.util.*; @@ -7,7 +7,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.*; // [databind#2283]: ignore read-only Lists even if "getter-as-setter" enabled -public class ReadOnlyList2283Test +public class ReadOnlyListDeser2283Test extends BaseMapTest { static class RenamedToSameOnGetter { diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/ReadOrWriteOnlyTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/ReadOrWriteOnlyTest.java index 0afd3ffb9..21f35c356 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/ReadOrWriteOnlyTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/ReadOrWriteOnlyTest.java @@ -1,8 +1,13 @@ package com.fasterxml.jackson.databind.deser; import java.beans.ConstructorProperties; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; + import com.fasterxml.jackson.databind.*; public class ReadOrWriteOnlyTest extends BaseMapTest @@ -66,6 +71,59 @@ public class ReadOrWriteOnlyTest extends BaseMapTest protected Foo1345() { } } + // [databind#1382] + static class Foo1382 { + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private List<Long> list = new ArrayList<>(); + + List<Long> getList() { + return list; + } + + public Foo1382 setList(List<Long> list) { + this.list = list; + return this; + } + } + + // [databind#1805] + static class UserWithReadOnly1805 { + public String name; + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public List<String> getRoles() { + return Arrays.asList("admin", "monitor"); + } + } + + // [databind#1805] + @JsonIgnoreProperties(value={ "roles" }, allowGetters=true) + static class UserAllowGetters1805 { + public String name; + + public List<String> getRoles() { + return Arrays.asList("admin", "monitor"); + } + } + + // [databind#2779]: ignorable property renaming + static class Bean2779 { + String works; + + @JsonProperty(value = "t", access = JsonProperty.Access.READ_ONLY) + public String getDoesntWork() { + return "pleaseFixThisBug"; + } + + public String getWorks() { + return works; + } + + public void setWorks(String works) { + this.works = works; + } + } + /* /********************************************************** /* Test methods @@ -93,6 +151,7 @@ public class ReadOrWriteOnlyTest extends BaseMapTest assertNotNull(result); } + // [databind#1345] public void testReadOnly1345() throws Exception { Foo1345 result = MAPPER.readValue("{\"name\":\"test\"}", Foo1345.class); @@ -100,4 +159,42 @@ public class ReadOrWriteOnlyTest extends BaseMapTest assertEquals("test", result.name); assertNull(result.id); } + + // [databind#1382] + public void testReadOnly1382() throws Exception + { + String payload = "{\"list\":[1,2,3,4]}"; + Foo1382 foo = MAPPER.readValue(payload, Foo1382.class); + assertTrue("List should be empty", foo.getList().isEmpty()); + } + + // [databind#1805] + public void testViaReadOnly() throws Exception { + UserWithReadOnly1805 user = new UserWithReadOnly1805(); + user.name = "foo"; + String json = MAPPER.writeValueAsString(user); + UserWithReadOnly1805 result = MAPPER.readValue(json, UserWithReadOnly1805.class); + assertNotNull(result); + } + + // [databind#1805] + public void testUsingAllowGetters() throws Exception { + UserAllowGetters1805 user = new UserAllowGetters1805(); + user.name = "foo"; + String json = MAPPER.writeValueAsString(user); + assertTrue(json.contains("roles")); + UserAllowGetters1805 result = MAPPER.readValue(json, UserAllowGetters1805.class); + assertNotNull(result); + } + + // [databind#2779]: ignorable property renaming + public void testIssue2779() throws Exception + { + Bean2779 bean = new Bean2779(); + bean.setWorks("works"); + + String json = MAPPER.writeValueAsString(bean); + Bean2779 newBean = MAPPER.readValue(json, Bean2779.class); + assertNotNull(newBean); + } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/TestBeanDeserializer.java b/src/test/java/com/fasterxml/jackson/databind/deser/TestBeanDeserializer.java index 988ece38d..05850407c 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/TestBeanDeserializer.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/TestBeanDeserializer.java @@ -322,7 +322,7 @@ public class TestBeanDeserializer extends BaseMapTest /******************************************************** */ - private final ObjectMapper MAPPER = new ObjectMapper(); + private final ObjectMapper MAPPER = newJsonMapper(); /** * Test to verify details of how trying to deserialize into @@ -370,24 +370,6 @@ public class TestBeanDeserializer extends BaseMapTest assertEquals(2, Issue476Deserializer.propCount); } - public void testPOJOFromEmptyString() throws Exception - { - // first, verify default settings which do not accept empty String: - assertFalse(MAPPER.isEnabled(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)); - try { - MAPPER.readValue(quote(""), Bean.class); - fail("Should not accept Empty String for POJO"); - } catch (JsonProcessingException e) { - verifyException(e, "from String value"); - assertValidLocation(e.getLocation()); - } - // should be ok to enable dynamically - ObjectReader r = MAPPER.readerFor(Bean.class) - .with(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT); - Bean result = r.readValue(quote("")); - assertNull(result); - } - // [databind#120] public void testModifyArrayDeserializer() throws Exception { diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/builder/BuilderErrorHandling.java b/src/test/java/com/fasterxml/jackson/databind/deser/builder/BuilderErrorHandling.java index f7f67b0c5..b71c1306a 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/builder/BuilderErrorHandling.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/builder/BuilderErrorHandling.java @@ -54,7 +54,7 @@ public class BuilderErrorHandling extends BaseMapTest MAPPER.readValue(json, ValueClassXY.class); fail("Should not pass"); } catch (MismatchedInputException e) { - verifyException(e, "unrecognized field"); + verifyException(e, "Unrecognized field"); } // but pass if ok to ignore ValueClassXY result = MAPPER.readerFor(ValueClassXY.class) @@ -63,4 +63,16 @@ public class BuilderErrorHandling extends BaseMapTest assertEquals(2, result._x); assertEquals(5, result._y); } + + public void testWrongShape() throws Exception + { + try { + MAPPER.readValue("123", ValueClassXY.class); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "Cannot construct instance of "); + // should report Builder class, not value here, right? + verifyException(e, "$SimpleBuilderXY"); + } + } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/builder/BuilderFailTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/builder/BuilderFailTest.java index 55f9feb92..743d52db6 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/builder/BuilderFailTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/builder/BuilderFailTest.java @@ -70,7 +70,8 @@ public class BuilderFailTest extends BaseMapTest MAPPER.readValue(json, ValueClassWrongBuildType.class); fail("Missing expected InvalidDefinitionException exception"); } catch (InvalidDefinitionException e) { - verifyException(e, "Build method"); + verifyException(e, "Build method "); + verifyException(e, "#build()"); verifyException(e, "has wrong return type"); } } diff --git a/src/test/java/com/fasterxml/jackson/failing/BuilderDeserializationTest921.java b/src/test/java/com/fasterxml/jackson/databind/deser/builder/BuilderWithTypeParametersTest.java index 79beaee6f..7b8ddf6fa 100644 --- a/src/test/java/com/fasterxml/jackson/failing/BuilderDeserializationTest921.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/builder/BuilderWithTypeParametersTest.java @@ -1,13 +1,17 @@ -package com.fasterxml.jackson.failing; +package com.fasterxml.jackson.databind.deser.builder; -import java.util.List; - -import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.LinkedHashMap; +import java.util.List; -public class BuilderDeserializationTest921 +// [databind#921]: support infering type parameters from Builder +public class BuilderWithTypeParametersTest extends BaseMapTest { public static class MyPOJO { @@ -33,6 +37,12 @@ public class BuilderDeserializationTest921 return data; } + // 28-Apr-2020, tatu: Note that as per [databind#921] the NAME of + // type variable here MUST match that of enclosing class. This has + // no semantic meaning to JDK or javac, but internally + // `MapperFeature.INFER_BUILDER_TYPE_BINDINGS` relies on this -- but + // can not really validate it. So user just has to rely on bit of + // black magic to use generic types with builders. public static class Builder<T> { private List<T> data; @@ -77,15 +87,30 @@ public class BuilderDeserializationTest921 } } - public void testWithBuilder() throws Exception { - final ObjectMapper mapper = new ObjectMapper(); + public void testWithBuilderInferringBindings() throws Exception { + final ObjectMapper mapper = jsonMapperBuilder() + .enable(MapperFeature.INFER_BUILDER_TYPE_BINDINGS) + .build(); + final String json = aposToQuotes("{ 'data': [ { 'x': 'x', 'y': 'y' } ] }"); + final MyGenericPOJO<MyPOJO> deserialized = + mapper.readValue(json, new TypeReference<MyGenericPOJO<MyPOJO>>() {}); + assertEquals(1, deserialized.data.size()); + Object ob = deserialized.data.get(0); + assertNotNull(ob); + assertEquals(MyPOJO.class, ob.getClass()); + } + + public void testWithBuilderWithoutInferringBindings() throws Exception { + final ObjectMapper mapper = jsonMapperBuilder() + .disable(MapperFeature.INFER_BUILDER_TYPE_BINDINGS) + .build(); final String json = aposToQuotes("{ 'data': [ { 'x': 'x', 'y': 'y' } ] }"); final MyGenericPOJO<MyPOJO> deserialized = mapper.readValue(json, new TypeReference<MyGenericPOJO<MyPOJO>>() {}); assertEquals(1, deserialized.data.size()); Object ob = deserialized.data.get(0); assertNotNull(ob); - assertEquals(MyPOJO.class, ob.getClass()); + assertEquals(LinkedHashMap.class, ob.getClass()); } public void testWithCreator() throws Exception { diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/creators/DelegatingArrayCreatorsTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/creators/DelegatingArrayCreatorsTest.java index c0ae77ca6..e33897e47 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/creators/DelegatingArrayCreatorsTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/creators/DelegatingArrayCreatorsTest.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.databind.BaseMapTest; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; public class DelegatingArrayCreatorsTest extends BaseMapTest { @@ -74,6 +75,20 @@ public class DelegatingArrayCreatorsTest extends BaseMapTest private Bag2324<Value2324> bagOfValues; } + static class MultipleArrayDelegators { + @JsonCreator(mode=JsonCreator.Mode.DELEGATING) + MultipleArrayDelegators(List<Integer> a) { } + + @JsonCreator(mode=JsonCreator.Mode.DELEGATING) + MultipleArrayDelegators(Set<Integer> a) { } + } + + /* + /********************************************************************** + /* Test methods + /********************************************************************** + */ + private final ObjectMapper MAPPER = sharedMapper(); // [databind#1804] @@ -96,4 +111,15 @@ public class DelegatingArrayCreatorsTest extends BaseMapTest assertEquals(3, result.getValues().size()); assertEquals(new Value2324("a"), result.getValues().iterator().next()); } + + public void testInvalidTwoArrayDelegating() throws Exception { + try { + /*MultipleArrayDelegators result =*/ MAPPER.readerFor(MultipleArrayDelegators.class) + .readValue("[ ]"); + fail("Should not pass"); + } catch (InvalidDefinitionException e) { + verifyException(e, "Conflicting array-delegate creators"); + verifyException(e, "already had explicitly marked"); + } + } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/creators/TestCreatorsDelegating.java b/src/test/java/com/fasterxml/jackson/databind/deser/creators/TestCreatorsDelegating.java index 065172064..edde73955 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/creators/TestCreatorsDelegating.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/creators/TestCreatorsDelegating.java @@ -26,7 +26,30 @@ public class TestCreatorsDelegating extends BaseMapTest } } - // for [JACKSON-711]; should allow delegate-based one(s) too + static class IntegerBean + { + protected Integer value; + + public IntegerBean(Integer v) { value = v; } + + @JsonCreator + protected static IntegerBean create(Integer value) { + return new IntegerBean(value); + } + } + + static class LongBean + { + protected Long value; + + public LongBean(Long v) { value = v; } + + @JsonCreator + protected static LongBean create(Long value) { + return new LongBean(value); + } + } + static class CtorBean711 { protected String name; @@ -40,7 +63,6 @@ public class TestCreatorsDelegating extends BaseMapTest } } - // for [JACKSON-711]; should allow delegate-based one(s) too static class FactoryBean711 { protected String name1; @@ -116,7 +138,7 @@ public class TestCreatorsDelegating extends BaseMapTest */ private final ObjectMapper MAPPER = newJsonMapper(); - + public void testBooleanDelegate() throws Exception { // should obviously work with booleans... @@ -127,8 +149,28 @@ public class TestCreatorsDelegating extends BaseMapTest bb = MAPPER.readValue(quote("true"), BooleanBean.class); assertEquals(Boolean.TRUE, bb.value); } - - // As per [JACKSON-711]: should also work with delegate model (single non-annotated arg) + + public void testIntegerDelegate() throws Exception + { + IntegerBean bb = MAPPER.readValue("-13", IntegerBean.class); + assertEquals(Integer.valueOf(-13), bb.value); + + // but also with value conversion from String (unless blocked) + bb = MAPPER.readValue(quote("127"), IntegerBean.class); + assertEquals(Integer.valueOf(127), bb.value); + } + + public void testLongDelegate() throws Exception + { + LongBean bb = MAPPER.readValue("11", LongBean.class); + assertEquals(Long.valueOf(11L), bb.value); + + // but also with value conversion from String (unless blocked) + bb = MAPPER.readValue(quote("-99"), LongBean.class); + assertEquals(Long.valueOf(-99L), bb.value); + } + + // should also work with delegate model (single non-annotated arg) public void testWithCtorAndDelegate() throws Exception { ObjectMapper mapper = new ObjectMapper(); diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/filter/IgnorePropertyOnDeserTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/filter/IgnorePropertyOnDeserTest.java index 953ea8678..a9589daf7 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/filter/IgnorePropertyOnDeserTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/filter/IgnorePropertyOnDeserTest.java @@ -10,6 +10,33 @@ import com.fasterxml.jackson.databind.ObjectMapper; public class IgnorePropertyOnDeserTest extends BaseMapTest { + // [databind#426] + @JsonIgnoreProperties({ "userId" }) + static class User { + public String firstName; + Integer userId; + + public Integer getUserId() { + return userId; + } + + public void setUserId(CharSequence id) { + userId = Integer.valueOf(id.toString()); + } + + public void setUserId(Integer v) { + this.userId = v; + } + + public void setUserId(User u) { + // bogus + } + + public void setUserId(boolean b) { + // bogus + } + } + // [databind#1217] static class IgnoreObject { public int x = 1; @@ -62,6 +89,16 @@ public class IgnorePropertyOnDeserTest extends BaseMapTest private final ObjectMapper MAPPER = newJsonMapper(); + // [databind#426] + public void testIssue426() throws Exception + { + final String JSON = aposToQuotes("{'userId': 9, 'firstName': 'Mike' }"); + User result = MAPPER.readerFor(User.class).readValue(JSON); + assertNotNull(result); + assertEquals("Mike", result.firstName); + assertNull(result.userId); + } + // [databind#1217] public void testIgnoreOnProperty1217() throws Exception { diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/filter/ProblemHandlerTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/filter/ProblemHandlerTest.java index 85beb40b3..815029a0c 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/filter/ProblemHandlerTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/filter/ProblemHandlerTest.java @@ -362,7 +362,7 @@ public class ProblemHandlerTest extends BaseMapTest mapper.readValue("true", Integer.class); fail("Should not pass"); } catch (MismatchedInputException e) { - verifyException(e, "out of VALUE_TRUE token"); + verifyException(e, "from Boolean value (token `JsonToken.VALUE_TRUE`)"); } } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/TestArrayDeserialization.java b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/ArrayDeserializationTest.java index b0fd99694..ac8838bd8 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/TestArrayDeserialization.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/ArrayDeserializationTest.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.databind.deser; +package com.fasterxml.jackson.databind.deser.jdk; import java.io.*; import java.util.*; @@ -16,7 +16,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; * This unit test suite tries to verify that the "Native" java type * mapper can properly re-construct Java array objects from Json arrays. */ -public class TestArrayDeserialization +public class ArrayDeserializationTest extends BaseMapTest { public final static class Bean1 diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/DateDeserializationTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/DateDeserializationTest.java index 2feba30b6..40a9cb958 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/DateDeserializationTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/DateDeserializationTest.java @@ -702,7 +702,7 @@ public class DateDeserializationTest fail("Did not throw exception when reading a value from a single value array with the UNWRAP_SINGLE_VALUE_ARRAYS feature disabled"); } catch (MismatchedInputException exp) { verifyException(exp, "Cannot deserialize"); - verifyException(exp, "out of START_ARRAY"); + verifyException(exp, "from Array value (token `JsonToken.START_ARRAY`)"); } reader = reader.with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS); diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKNumberDeserTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKNumberDeserTest.java index e31636741..4b551193a 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKNumberDeserTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKNumberDeserTest.java @@ -7,7 +7,10 @@ import java.math.BigInteger; import java.util.List; import java.util.Map; +import com.fasterxml.jackson.annotation.*; + import com.fasterxml.jackson.core.*; + import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.exc.MismatchedInputException; @@ -37,6 +40,42 @@ public class JDKNumberDeserTest extends BaseMapTest public MyBeanValue(BigDecimal d) { this.decimal = d; } } + // [databind#2644] + static class NodeRoot2644 { + public String type; + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type") + @JsonSubTypes(value = { + @JsonSubTypes.Type(value = NodeParent2644.class, name = "NodeParent") + }) + public Node2644 node; + } + + public static class NodeParent2644 extends Node2644 { } + + public static abstract class Node2644 { + @JsonProperty("amount") + BigDecimal val; + + public BigDecimal getVal() { + return val; + } + + public void setVal(BigDecimal val) { + this.val = val; + } + } + + // [databind#2784] + static class BigDecimalHolder2784 { + public BigDecimal value; + } + + static class NestedBigDecimalHolder2784 { + @JsonUnwrapped + public BigDecimalHolder2784 holder; + } + /* /********************************************************************** /* Helper classes, serializers/deserializers/resolvers @@ -138,7 +177,7 @@ public class JDKNumberDeserTest extends BaseMapTest noCoerceMapper.readValue(NULL_JSON, Integer.TYPE); fail("Should not have passed"); } catch (MismatchedInputException e) { - verifyException(e, "Cannot coerce String \"null\""); + verifyException(e, "Cannot coerce String value"); } } @@ -299,4 +338,28 @@ public class JDKNumberDeserTest extends BaseMapTest } assertEquals(42, node.asInt()); } + + // [databind#2644] + public void testBigDecimalSubtypes() throws Exception + { + ObjectMapper mapper = jsonMapperBuilder() + .registerSubtypes(NodeParent2644.class) + .build(); + NodeRoot2644 root = mapper.readValue( + "{\"type\": \"NodeParent\",\"node\": {\"amount\": 9999999999999999.99} }", + NodeRoot2644.class + ); + + assertEquals(new BigDecimal("9999999999999999.99"), root.node.getVal()); + } + + // [databind#2784] + public void testBigDecimalUnwrapped() throws Exception + { + final ObjectMapper mapper = newJsonMapper(); + // mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); + final String JSON = "{\"value\": 5.00}"; + NestedBigDecimalHolder2784 result = mapper.readValue(JSON, NestedBigDecimalHolder2784.class); + assertEquals(new BigDecimal("5.00"), result.holder.value); + } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKNumberLeniencyTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKNumberLeniencyTest.java index b7269cbf7..47d8bb410 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKNumberLeniencyTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKNumberLeniencyTest.java @@ -32,7 +32,7 @@ public class JDKNumberLeniencyTest extends BaseMapTest fail("Should not allow read in strict mode"); } catch (MismatchedInputException e) { verifyException(e, "Cannot coerce"); - verifyException(e, "for type `java.lang.Boolean`"); + verifyException(e, "to `java.lang.Boolean` value"); } } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKScalarsTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKScalarsTest.java index cdf9ec1ac..7e244a0e3 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKScalarsTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKScalarsTest.java @@ -2,6 +2,8 @@ package com.fasterxml.jackson.databind.deser.jdk; import java.io.*; import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.List; import org.junit.Assert; @@ -12,6 +14,7 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.JsonMappingException.Reference; import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.fasterxml.jackson.databind.util.ClassUtil; /** * Unit tests for verifying handling of simple basic non-structured @@ -120,6 +123,10 @@ public class JDKScalarsTest private final ObjectMapper MAPPER = newJsonMapper(); + private final ObjectMapper MAPPER_NO_COERCION = jsonMapperBuilder() + .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS) + .build(); + /* /********************************************************** /* Scalar tests for boolean @@ -226,12 +233,15 @@ public class JDKScalarsTest public void testCharacterWrapper() throws Exception { // First: canonical value is 1-char string - Character result = MAPPER.readValue("\"a\"", Character.class); - assertEquals(Character.valueOf('a'), result); + assertEquals(Character.valueOf('a'), MAPPER.readValue(quote("a"), Character.class)); // But can also pass in ascii code - result = MAPPER.readValue(" "+((int) 'X'), Character.class); + Character result = MAPPER.readValue(" "+((int) 'X'), Character.class); assertEquals(Character.valueOf('X'), result); + + // 22-Jun-2020, tatu: one special case turns out to be white space; + // need to avoid considering it "blank" value + assertEquals(Character.valueOf(' '), MAPPER.readValue(quote(" "), Character.class)); final CharacterWrapperBean wrapper = MAPPER.readValue("{\"v\":null}", CharacterWrapperBean.class); assertNotNull(wrapper); @@ -431,30 +441,6 @@ public class JDKScalarsTest /********************************************************** */ - public void testEmptyToNullCoercionForPrimitives() throws Exception { - _testEmptyToNullCoercion(int.class, Integer.valueOf(0)); - _testEmptyToNullCoercion(long.class, Long.valueOf(0)); - _testEmptyToNullCoercion(double.class, Double.valueOf(0.0)); - _testEmptyToNullCoercion(float.class, Float.valueOf(0.0f)); - } - - private void _testEmptyToNullCoercion(Class<?> primType, Object emptyValue) throws Exception - { - final String EMPTY = "\"\""; - - // as per [databind#1095] should only allow coercion from empty String, - // if `null` is acceptable - ObjectReader intR = MAPPER.readerFor(primType); - assertEquals(emptyValue, intR.readValue(EMPTY)); - try { - intR.with(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) - .readValue("\"\""); - fail("Should not have passed"); - } catch (MismatchedInputException e) { - verifyException(e, "Cannot coerce empty String"); - } - } - public void testBase64Variants() throws Exception { final byte[] INPUT = "abcdefghijklmnopqrstuvwxyz1234567890abcdefghijklmnopqrstuvwxyz1234567890X".getBytes("UTF-8"); @@ -515,13 +501,15 @@ public class JDKScalarsTest */ // by default, should return nulls, n'est pas? - public void testEmptyStringForWrappers() throws IOException + public void testEmptyStringForBooleanWrapper() throws IOException { - WrappersBean bean; - - bean = MAPPER.readValue("{\"booleanValue\":\"\"}", WrappersBean.class); + WrappersBean bean = MAPPER.readValue("{\"booleanValue\":\"\"}", WrappersBean.class); assertNull(bean.booleanValue); - bean = MAPPER.readValue("{\"byteValue\":\"\"}", WrappersBean.class); + } + + public void testEmptyStringForIntegerWrappers() throws IOException + { + WrappersBean bean = MAPPER.readValue("{\"byteValue\":\"\"}", WrappersBean.class); assertNull(bean.byteValue); // char/Character is different... not sure if this should work or not: @@ -534,18 +522,25 @@ public class JDKScalarsTest assertNull(bean.intValue); bean = MAPPER.readValue("{\"longValue\":\"\"}", WrappersBean.class); assertNull(bean.longValue); - bean = MAPPER.readValue("{\"floatValue\":\"\"}", WrappersBean.class); + } + + public void testEmptyStringForFloatWrappers() throws IOException + { + WrappersBean bean = MAPPER.readValue("{\"floatValue\":\"\"}", WrappersBean.class); assertNull(bean.floatValue); bean = MAPPER.readValue("{\"doubleValue\":\"\"}", WrappersBean.class); assertNull(bean.doubleValue); } - public void testEmptyStringForPrimitives() throws IOException + public void testEmptyStringForBooleanPrimitive() throws IOException { - PrimitivesBean bean; - bean = MAPPER.readValue("{\"booleanValue\":\"\"}", PrimitivesBean.class); + PrimitivesBean bean = MAPPER.readValue("{\"booleanValue\":\"\"}", PrimitivesBean.class); assertFalse(bean.booleanValue); - bean = MAPPER.readValue("{\"byteValue\":\"\"}", PrimitivesBean.class); + } + + public void testEmptyStringForIntegerPrimitives() throws IOException + { + PrimitivesBean bean = MAPPER.readValue("{\"byteValue\":\"\"}", PrimitivesBean.class); assertEquals((byte) 0, bean.byteValue); bean = MAPPER.readValue("{\"charValue\":\"\"}", PrimitivesBean.class); assertEquals((char) 0, bean.charValue); @@ -555,38 +550,30 @@ public class JDKScalarsTest assertEquals(0, bean.intValue); bean = MAPPER.readValue("{\"longValue\":\"\"}", PrimitivesBean.class); assertEquals(0L, bean.longValue); - bean = MAPPER.readValue("{\"floatValue\":\"\"}", PrimitivesBean.class); + } + + public void testEmptyStringForFloatPrimitives() throws IOException + { + PrimitivesBean bean = MAPPER.readValue("{\"floatValue\":\"\"}", PrimitivesBean.class); assertEquals(0.0f, bean.floatValue); bean = MAPPER.readValue("{\"doubleValue\":\"\"}", PrimitivesBean.class); assertEquals(0.0, bean.doubleValue); } - private void _verifyEmptyStringFailForPrimitives(String propName) throws IOException + // for [databind#403] + public void testEmptyStringFailForBooleanPrimitive() throws IOException { final ObjectReader reader = MAPPER .readerFor(PrimitivesBean.class) .with(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES); try { - reader.readValue(aposToQuotes("{'"+propName+"':''}")); + reader.readValue(aposToQuotes("{'booleanValue':''}")); fail("Expected failure for boolean + empty String"); } catch (JsonMappingException e) { - verifyException(e, "Cannot coerce empty String (\"\")"); - verifyException(e, "to Null value"); + verifyException(e, "Cannot coerce `null` to `boolean`"); + verifyException(e, "FAIL_ON_NULL_FOR_PRIMITIVES"); } } - - // for [databind#403] - public void testEmptyStringFailForPrimitives() throws IOException - { - _verifyEmptyStringFailForPrimitives("booleanValue"); - _verifyEmptyStringFailForPrimitives("byteValue"); - _verifyEmptyStringFailForPrimitives("charValue"); - _verifyEmptyStringFailForPrimitives("shortValue"); - _verifyEmptyStringFailForPrimitives("intValue"); - _verifyEmptyStringFailForPrimitives("longValue"); - _verifyEmptyStringFailForPrimitives("floatValue"); - _verifyEmptyStringFailForPrimitives("doubleValue"); - } /* /********************************************************** @@ -722,18 +709,18 @@ public class JDKScalarsTest final String JSON_WITH_NULL = "[ null ]"; final String SIMPLE_NAME = "`"+cls.getSimpleName()+"`"; final ObjectReader readerCoerceOk = MAPPER.readerFor(cls); - final ObjectReader readerNoCoerce = readerCoerceOk + final ObjectReader readerNoNulls = readerCoerceOk .with(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES); Object ob = readerCoerceOk.forType(cls).readValue(JSON_WITH_NULL); assertEquals(1, Array.getLength(ob)); assertEquals(defValue, Array.get(ob, 0)); try { - readerNoCoerce.readValue(JSON_WITH_NULL); + readerNoNulls.readValue(JSON_WITH_NULL); fail("Should not pass"); } catch (JsonMappingException e) { verifyException(e, "Cannot coerce `null`"); - verifyException(e, "as content of type "+SIMPLE_NAME); + verifyException(e, "to element of "+SIMPLE_NAME); } if (testEmptyString) { @@ -741,12 +728,14 @@ public class JDKScalarsTest assertEquals(1, Array.getLength(ob)); assertEquals(defValue, Array.get(ob, 0)); + final ObjectReader readerNoEmpty = MAPPER_NO_COERCION.readerFor(cls); try { - readerNoCoerce.readValue(EMPTY_STRING_JSON); + readerNoEmpty.readValue(EMPTY_STRING_JSON); fail("Should not pass"); } catch (JsonMappingException e) { - verifyException(e, "Cannot coerce empty String (\"\")"); - verifyException(e, "as content of type "+SIMPLE_NAME); + // 07-Jun-2020, tatu: during transition, two acceptable alternatives + verifyException(e, "Cannot coerce `null` to", "Cannot coerce empty String (\"\")"); + verifyException(e, "element of "+SIMPLE_NAME); } } } @@ -782,11 +771,11 @@ public class JDKScalarsTest // char[] is special, cannot use generalized test here // _testInvalidStringCoercionFail(char[].class); - _testInvalidStringCoercionFail(short[].class); - _testInvalidStringCoercionFail(int[].class); - _testInvalidStringCoercionFail(long[].class); - _testInvalidStringCoercionFail(float[].class); - _testInvalidStringCoercionFail(double[].class); + _testInvalidStringCoercionFail(short[].class, "short"); + _testInvalidStringCoercionFail(int[].class, "int"); + _testInvalidStringCoercionFail(long[].class, "long"); + _testInvalidStringCoercionFail(float[].class, "float"); + _testInvalidStringCoercionFail(double[].class, "double"); } private void _testInvalidStringCoercionFail(Class<?> cls) throws IOException @@ -801,9 +790,38 @@ public class JDKScalarsTest try { MAPPER.readerFor(cls).readValue(JSON); - fail("Should not pass"); + fail("Should MismatchedInputException pass"); } catch (JsonMappingException e) { verifyException(e, "Cannot deserialize value of type `"+targetTypeName+"` from String \"foobar\""); } } + + /* + /********************************************************** + /* Tests for mismatch: JSON Object for scalars (not supported + /* for JSON + /********************************************************** + */ + + public void testFailForScalarFromObject() throws Exception + { + _testFailForNumberFromObject(Byte.TYPE); + _testFailForNumberFromObject(Short.TYPE); + _testFailForNumberFromObject(Long.TYPE); + _testFailForNumberFromObject(Float.TYPE); + _testFailForNumberFromObject(Double.TYPE); + _testFailForNumberFromObject(BigInteger.class); + _testFailForNumberFromObject(BigDecimal.class); + } + + private void _testFailForNumberFromObject(Class<?> targetType) throws Exception + { + try { + MAPPER.readValue(a2q("{'value':12}"), targetType); + fail("Should not pass"); + } catch (MismatchedInputException e) { + verifyException(e, "from Object value"); + verifyException(e, ClassUtil.getClassDescription(targetType)); + } + } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKStringLikeTypesTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKStringLikeTypesTest.java index b4e5892bd..9af210d3c 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKStringLikeTypesTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/JDKStringLikeTypesTest.java @@ -138,10 +138,6 @@ public class JDKStringLikeTypesTest extends BaseMapTest MAPPER.readValue(quote("fi_FI_savo"), Locale.class)); assertEquals(new Locale("en", "US"), MAPPER.readValue(quote("en-US"), Locale.class)); - - // [databind#1123] - Locale loc = MAPPER.readValue(quote(""), Locale.class); - assertSame(Locale.ROOT, loc); } public void testCharSequence() throws IOException @@ -261,14 +257,9 @@ public class JDKStringLikeTypesTest extends BaseMapTest final URI value = new URI("http://foo.com"); assertEquals(value, reader.readValue("\""+value.toString()+"\"")); - // Also: empty String should be handled properly - URI result = reader.readValue(quote("")); - assertNotNull(result); - assertEquals(URI.create(""), result); - // and finally: broken URI should give proper failure try { - result = reader.readValue(quote("a b")); + URI result = reader.readValue(quote("a b")); fail("Should not accept malformed URI, instead got: "+result); } catch (InvalidFormatException e) { verifyException(e, "not a valid textual representation"); @@ -303,9 +294,9 @@ public class JDKStringLikeTypesTest extends BaseMapTest public void testUUID() throws Exception { - final ObjectMapper mapper = objectMapper(); - final String NULL_UUID = "00000000-0000-0000-0000-000000000000"; + final ObjectReader r = MAPPER.readerFor(UUID.class); + // first, couple of generated UUIDs: for (String value : new String[] { "76e6d183-5f68-4afa-b94a-922c1fdb83f8", @@ -315,12 +306,10 @@ public class JDKStringLikeTypesTest extends BaseMapTest "82994ac2-7b23-49f2-8cc5-e24cf6ed77be", "00000007-0000-0000-0000-000000000000" }) { - - mapper.disable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS); - UUID uuid = UUID.fromString(value); assertEquals(uuid, - mapper.readValue(quote(value), UUID.class)); + r.without(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS) + .readValue(quote(value))); } // then use templating; note that these are not exactly valid UUIDs // wrt spec (type bits etc), but JDK UUID should deal ok @@ -330,13 +319,13 @@ public class JDKStringLikeTypesTest extends BaseMapTest for (int i = 0; i < chars.length(); ++i) { String value = TEMPL.replace('0', chars.charAt(i)); assertEquals(UUID.fromString(value).toString(), - mapper.readValue(quote(value), UUID.class).toString()); + r.readValue(quote(value)).toString()); } // also: see if base64 encoding works as expected String base64 = Base64Variants.getDefaultVariant().encode(new byte[16]); assertEquals(UUID.fromString(NULL_UUID), - mapper.readValue(quote(base64), UUID.class)); + r.readValue(quote(base64))); } public void testUUIDInvalid() throws Exception diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/TestGenericMapDeser.java b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/MapWithGenericValuesDeserTest.java index e18dd2575..db6ad7700 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/TestGenericMapDeser.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/MapWithGenericValuesDeserTest.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.databind.deser; +package com.fasterxml.jackson.databind.deser.jdk; import java.util.*; @@ -9,7 +9,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.type.TypeFactory; @SuppressWarnings("serial") -public class TestGenericMapDeser +public class MapWithGenericValuesDeserTest extends BaseMapTest { /* diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/jdk/VoidProperties2675Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/VoidProperties2675Test.java new file mode 100644 index 000000000..1aae71209 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/jdk/VoidProperties2675Test.java @@ -0,0 +1,61 @@ +package com.fasterxml.jackson.databind.deser.jdk; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; + +// [databind#2675]: Void-valued "properties" +public class VoidProperties2675Test extends BaseMapTest +{ + static class VoidBean { + protected Void value; + + public Void getValue() { return null; } + +// public void setValue(Void v) { } + } + + /* + /********************************************************************** + /* Test methods + /********************************************************************** + */ + + private final ObjectMapper DEFAULT_MAPPER = sharedMapper(); + + private final ObjectMapper VOID_MAPPER = jsonMapperBuilder() + .enable(MapperFeature.ALLOW_VOID_VALUED_PROPERTIES) + .build(); + + public void testVoidBeanSerialization() throws Exception + { + // By default (2.x), not enabled: + try { + DEFAULT_MAPPER.writeValueAsString(new VoidBean()); + fail("Should not pass"); + } catch (InvalidDefinitionException e) { + verifyException(e, "no properties discovered"); + } + + // but when enabled + assertEquals("{\"value\":null}", VOID_MAPPER.writeValueAsString(new VoidBean())); + } + + public void testVoidBeanDeserialization() throws Exception { + final String DOC = "{\"value\":null}"; + VoidBean result; + + // By default (2.x), not enabled: + try { + result = DEFAULT_MAPPER.readValue(DOC, VoidBean.class); + fail("Should not pass"); + } catch (UnrecognizedPropertyException e) { + verifyException(e, "Unrecognized field \"value\""); + } + + // but when enabled + result = VOID_MAPPER.readValue(DOC, VoidBean.class); + assertNotNull(result); + assertNull(result.getValue()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/exc/ExceptionDeserializationTest.java b/src/test/java/com/fasterxml/jackson/databind/exc/ExceptionDeserializationTest.java index 536a5b428..7e19c9a4c 100644 --- a/src/test/java/com/fasterxml/jackson/databind/exc/ExceptionDeserializationTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/exc/ExceptionDeserializationTest.java @@ -157,7 +157,7 @@ public class ExceptionDeserializationTest mapper.readValue(value, IOException.class); fail("Exception not thrown when attempting to deserialize an IOException wrapped in a single value array with UNWRAP_SINGLE_VALUE_ARRAYS disabled"); } catch (JsonMappingException exp2) { - verifyException(exp2, "out of START_ARRAY"); + verifyException(exp2, "from Array value (token `JsonToken.START_ARRAY`)"); } } diff --git a/src/test/java/com/fasterxml/jackson/databind/interop/DateJava8FallbacksTest.java b/src/test/java/com/fasterxml/jackson/databind/interop/DateJava8FallbacksTest.java new file mode 100644 index 000000000..37fd4e79c --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/interop/DateJava8FallbacksTest.java @@ -0,0 +1,45 @@ +package com.fasterxml.jackson.databind.interop; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; + +// [databind#2683]: add fallback handling for Java 8 date/time types, to +// prevent accidental serialization as POJOs, as well as give more information +// on deserialization attempts +// +// @since 2.12 +public class DateJava8FallbacksTest extends BaseMapTest +{ + private final ObjectMapper MAPPER = newJsonMapper(); + + private final OffsetDateTime DATETIME_EPOCH = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), + ZoneOffset.of("Z")); + + // Test to prevent serialization as POJO, without Java 8 date/time module: + public void testPreventSerialization() throws Exception + { + try { + String json = MAPPER.writerWithDefaultPrettyPrinter() + .writeValueAsString(DATETIME_EPOCH); + fail("Should not pass, wrote out as\n: "+json); + } catch (InvalidDefinitionException e) { + verifyException(e, "Java 8 date/time type `java.time.OffsetDateTime` not supported by default"); + verifyException(e, "add Module \"com.fasterxml.jackson.datatype:jackson-datatype-jsr310\""); + } + } + + public void testBetterDeserializationError() throws Exception + { + try { + OffsetDateTime result = MAPPER.readValue(" 0 ", OffsetDateTime.class); + fail("Not expecting to pass, resulted in: "+result); + } catch (InvalidDefinitionException e) { + verifyException(e, "Java 8 date/time type `java.time.OffsetDateTime` not supported by default"); + verifyException(e, "add Module \"com.fasterxml.jackson.datatype:jackson-datatype-jsr310\""); + } + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestMultipleTypeNames.java b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestMultipleTypeNames.java new file mode 100644 index 000000000..021dd1cb2 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestMultipleTypeNames.java @@ -0,0 +1,136 @@ +package com.fasterxml.jackson.databind.jsontype; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; + +import java.util.List; + +// Tests for [databind#2761] (and [annotations#171] +public class TestMultipleTypeNames extends BaseMapTest +{ + private final ObjectMapper MAPPER = newJsonMapper(); + + // common classes + static class MultiTypeName { } + + static class A extends MultiTypeName { + private long x; + public long getX() { return x; } + } + + static class B extends MultiTypeName { + private float y; + public float getY() { return y; } + } + + // data for test 1 + static class WrapperForNamesTest { + private List<BaseForNamesTest> base; + public List<BaseForNamesTest> getBase() { return base; } + } + + static class BaseForNamesTest { + private String type; + public String getType() { return type; } + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXTERNAL_PROPERTY, + property = "type" + ) + @JsonSubTypes(value = { + @JsonSubTypes.Type(value = A.class, names = "a"), + @JsonSubTypes.Type(value = B.class, names = {"b","c"}), + }) + private MultiTypeName data; + public MultiTypeName getData() { return data; } + } + + static class WrapperForNameAndNamesTest { + private List<BaseForNameAndNamesTest> base; + public List<BaseForNameAndNamesTest> getBase() { return base; } + } + + static class BaseForNameAndNamesTest { + private String type; + public String getType() { return type; } + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXTERNAL_PROPERTY, + property = "type" + ) + @JsonSubTypes(value = { + @JsonSubTypes.Type(value = A.class, name = "a"), + @JsonSubTypes.Type(value = B.class, names = {"b","c"}), + }) + private MultiTypeName data; + public MultiTypeName getData() { return data; } + } + + /* + /********************************************************** + /* Test methods + /********************************************************** + */ + + public void testOnlyNames() throws Exception + { + String json; + WrapperForNamesTest w; + + // TC 1 : all KV serialisation + json = "{\"base\": [{\"type\":\"a\", \"data\": {\"x\": 5}}, {\"type\":\"b\", \"data\": {\"y\": 3.1}}, {\"type\":\"c\", \"data\": {\"y\": 33.8}}]}"; + w = MAPPER.readValue(json, WrapperForNamesTest.class); + assertNotNull(w); + assertEquals(3, w.base.size()); + assertTrue(w.base.get(0).data instanceof A); + assertEquals(5l, ((A) w.base.get(0).data).x); + assertTrue(w.base.get(1).data instanceof B); + assertEquals(3.1f, ((B) w.base.get(1).data).y, 0); + assertTrue(w.base.get(2).data instanceof B); + assertEquals(33.8f, ((B) w.base.get(2).data).y, 0); + + + // TC 2 : incorrect serialisation + json = "{\"data\": [{\"type\":\"a\", \"data\": {\"x\": 2.2}}, {\"type\":\"b\", \"data\": {\"y\": 5.3}}, {\"type\":\"c\", \"data\": {\"y\": 9.8}}]}"; + try { + MAPPER.readValue(json, WrapperForNamesTest.class); + fail("This serialisation should fail 'coz of x being float"); + } catch (UnrecognizedPropertyException e) { + verifyException(e, "Unrecognized field \"data\""); + } + } + + public void testNameAndNames() throws Exception + { + String json; + WrapperForNameAndNamesTest w; + + // TC 1 : all KV serialisation + json = "{\"base\": [{\"type\":\"a\", \"data\": {\"x\": 5}}, {\"type\":\"b\", \"data\": {\"y\": 3.1}}, {\"type\":\"c\", \"data\": {\"y\": 33.8}}]}"; + w = MAPPER.readValue(json, WrapperForNameAndNamesTest.class); + assertNotNull(w); + assertEquals(3, w.base.size()); + assertTrue(w.base.get(0).data instanceof A); + assertEquals(5l, ((A) w.base.get(0).data).x); + assertTrue(w.base.get(1).data instanceof B); + assertEquals(3.1f, ((B) w.base.get(1).data).y, 0); + assertTrue(w.base.get(2).data instanceof B); + assertEquals(33.8f, ((B) w.base.get(2).data).y, 0); + + + // TC 2 : incorrect serialisation + json = "{\"data\": [{\"type\":\"a\", \"data\": {\"x\": 2.2}}, {\"type\":\"b\", \"data\": {\"y\": 5.3}}, {\"type\":\"c\", \"data\": {\"y\": 9.8}}]}"; + try { + MAPPER.readValue(json, WrapperForNameAndNamesTest.class); + fail("This serialisation should fail 'coz of x being float"); + } catch (UnrecognizedPropertyException e) { + verifyException(e, "Unrecognized field \"data\""); + } + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestSubtypes.java b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestSubtypes.java index c6fa2bb35..4557e0840 100644 --- a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestSubtypes.java +++ b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestSubtypes.java @@ -4,12 +4,9 @@ import com.fasterxml.jackson.core.Version; import java.util.*; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; -import com.fasterxml.jackson.databind.JsonMappingException; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; @@ -95,7 +92,9 @@ public class TestSubtypes extends com.fasterxml.jackson.databind.BaseMapTest @JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=As.PROPERTY, property="type") @JsonSubTypes({ @JsonSubTypes.Type(ImplX.class), - @JsonSubTypes.Type(ImplY.class) }) + @JsonSubTypes.Type(ImplY.class), + @JsonSubTypes.Type(ImplAbs.class) + }) static abstract class BaseX { } @JsonTypeName("x") @@ -111,6 +110,11 @@ public class TestSubtypes extends com.fasterxml.jackson.databind.BaseMapTest public int y; } + // for [databind#919] testing + @JsonTypeName("abs") + abstract static class ImplAbs extends BaseX { + } + // [databind#663] static class AtomicWrapper { public BaseX value; @@ -372,16 +376,16 @@ public class TestSubtypes extends com.fasterxml.jackson.databind.BaseMapTest bean = mapper.readValue("{\"#type\":\"foobar\"}", SuperTypeWithoutDefault.class); assertEquals(DefaultImpl505.class, bean.getClass()); assertEquals(0, ((DefaultImpl505) bean).a); - } - + public void testErrorMessage() throws Exception { ObjectMapper mapper = new ObjectMapper(); try { mapper.readValue("{ \"type\": \"z\"}", BaseX.class); fail("Should have failed"); - } catch (JsonMappingException e) { - verifyException(e, "known type ids ="); + } catch (InvalidTypeIdException e) { + verifyException(e, "Could not resolve type id 'z' as a subtype of"); + verifyException(e, "known type ids = [x, y]"); } } diff --git a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestWithGenerics.java b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestWithGenerics.java index 15d95f82d..8a066f6d2 100644 --- a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestWithGenerics.java +++ b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestWithGenerics.java @@ -126,7 +126,7 @@ public class TestWithGenerics extends BaseMapTest otherAnimal = a2; } } - + /* /********************************************************** /* Unit tests diff --git a/src/test/java/com/fasterxml/jackson/databind/misc/CaseInsensitiveDeserTest.java b/src/test/java/com/fasterxml/jackson/databind/misc/CaseInsensitiveDeserTest.java index 362039391..dbb738ca7 100644 --- a/src/test/java/com/fasterxml/jackson/databind/misc/CaseInsensitiveDeserTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/misc/CaseInsensitiveDeserTest.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; public class CaseInsensitiveDeserTest extends BaseMapTest { @@ -93,13 +94,31 @@ public class CaseInsensitiveDeserTest extends BaseMapTest } } + // [databind#1886]: allow case-insensitivity by default on a class + @JsonFormat(with={ JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES }) + static class CaseInsensitiveRole { + public String ID; + public String Name; + } + + // [databind#1886]: allow case-insensitivity by default on a class + static class CaseInsensitiveRoleContainer { + public CaseInsensitiveRole role; + } + + // [databind#1886]: ... but also overrides + static class CaseSensitiveRoleContainer { + @JsonFormat(without={ JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES }) + public CaseInsensitiveRole role; + } + /* /******************************************************** /* Test methods /******************************************************** */ - private final ObjectMapper MAPPER = new ObjectMapper(); + private final ObjectMapper MAPPER = newJsonMapper(); private final ObjectMapper INSENSITIVE_MAPPER = jsonMapperBuilder() .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) .build(); @@ -158,7 +177,7 @@ public class CaseInsensitiveDeserTest extends BaseMapTest } // And allow config overrides too - public void testCaseInsensitiveWithClassFormat() throws Exception + public void testCaseInsensitiveViaConfigOverride() throws Exception { ObjectMapper mapper = new ObjectMapper(); mapper.configOverride(Role.class) @@ -181,4 +200,34 @@ public class CaseInsensitiveDeserTest extends BaseMapTest assertNotNull(result.getItems()); assertEquals(1, result.getItems().size()); } + + + // [databind#1886]: allow case-insensitivity by default on a class + public void testCaseInsensitiveViaClassAnnotation() throws Exception + { + final String CONTAINED = aposToQuotes("{'role': {'id':'3','name':'Bob'}}"); + + // First: via wrapper/container: + CaseInsensitiveRoleContainer cont = MAPPER.readValue(CONTAINED, + CaseInsensitiveRoleContainer.class); + assertEquals("3", cont.role.ID); + assertEquals("Bob", cont.role.Name); + + // second: directly as root value + CaseInsensitiveRole role = MAPPER.readValue + (aposToQuotes("{'id':'12','name':'Billy'}"), + CaseInsensitiveRole.class); + assertEquals("12", role.ID); + assertEquals("Billy", role.Name); + + // and finally, more complicated; should be possible to force sensitivity: + try { + /*CaseSensitiveRoleContainer r =*/ MAPPER.readValue(CONTAINED, + CaseSensitiveRoleContainer.class); + fail("Should not pass"); + } catch (UnrecognizedPropertyException e) { + verifyException(e, "Unrecognized "); + verifyException(e, "\"id\""); + } + } } diff --git a/src/test/java/com/fasterxml/jackson/databind/node/ArrayNodeTest.java b/src/test/java/com/fasterxml/jackson/databind/node/ArrayNodeTest.java index e129dcf56..08bd47d98 100644 --- a/src/test/java/com/fasterxml/jackson/databind/node/ArrayNodeTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/node/ArrayNodeTest.java @@ -359,7 +359,7 @@ public class ArrayNodeTest mapper.readValue(" 123 ", ArrayNode.class); fail("Should not pass"); } catch (MismatchedInputException e) { - verifyException(e, "out of VALUE_NUMBER_INT token"); + verifyException(e, "from Integer value (token `JsonToken.VALUE_NUMBER_INT`)"); } } } diff --git a/src/test/java/com/fasterxml/jackson/databind/node/ObjectNodeTest.java b/src/test/java/com/fasterxml/jackson/databind/node/ObjectNodeTest.java index 260e93ecc..cd15ee687 100644 --- a/src/test/java/com/fasterxml/jackson/databind/node/ObjectNodeTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/node/ObjectNodeTest.java @@ -463,7 +463,7 @@ public class ObjectNodeTest mapper.readValue("[ 1, 2, 3 ]", ObjectNode.class); fail("Should not pass"); } catch (MismatchedInputException e) { - verifyException(e, "out of START_ARRAY token"); + verifyException(e, "from Array value (token `JsonToken.START_ARRAY`)"); } } } diff --git a/src/test/java/com/fasterxml/jackson/databind/objectid/ObjectId687Test.java b/src/test/java/com/fasterxml/jackson/databind/objectid/ObjectId687Test.java index 972a5fa8f..91eda5e44 100644 --- a/src/test/java/com/fasterxml/jackson/databind/objectid/ObjectId687Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/objectid/ObjectId687Test.java @@ -63,7 +63,7 @@ public class ObjectId687Test extends BaseMapTest /***************************************************** */ - private final ObjectMapper MAPPER = objectMapper(); + private final ObjectMapper MAPPER = newJsonMapper(); // for [databind#687] public void testSerializeDeserializeWithCreator() throws IOException { diff --git a/src/test/java/com/fasterxml/jackson/databind/objectid/ObjectId825BTest.java b/src/test/java/com/fasterxml/jackson/databind/objectid/ObjectId825BTest.java index af173eede..171f1ded1 100644 --- a/src/test/java/com/fasterxml/jackson/databind/objectid/ObjectId825BTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/objectid/ObjectId825BTest.java @@ -136,10 +136,15 @@ public class ObjectId825BTest extends BaseMapTest private static final long serialVersionUID = 1L; } + /* + /***************************************************** + /* Test methods + /***************************************************** + */ + public void testFull825() throws Exception { final ObjectMapper mapper = jsonMapperBuilder() - .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT) .activateDefaultTyping(NoCheckSubTypeValidator.instance, ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE) .build(); diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/TestArraySerialization.java b/src/test/java/com/fasterxml/jackson/databind/ser/TestArraySerialization.java index 10baa8ef6..e1cd38d69 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ser/TestArraySerialization.java +++ b/src/test/java/com/fasterxml/jackson/databind/ser/TestArraySerialization.java @@ -6,7 +6,7 @@ import com.fasterxml.jackson.databind.*; public class TestArraySerialization extends BaseMapTest { - private final ObjectMapper MAPPER = newJsonMapper(); + private final ObjectMapper MAPPER = sharedMapper(); public void testLongStringArray() throws Exception { diff --git a/src/test/java/com/fasterxml/jackson/databind/struct/EmptyArrayAsNullTest.java b/src/test/java/com/fasterxml/jackson/databind/struct/EmptyArrayAsNullTest.java deleted file mode 100644 index 71aba3253..000000000 --- a/src/test/java/com/fasterxml/jackson/databind/struct/EmptyArrayAsNullTest.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.fasterxml.jackson.databind.struct; - -import java.math.BigDecimal; -import java.math.BigInteger; - -import java.util.*; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.*; - -/** - * Tests to verify implementation of [databind#540]; also for - * follow up work of: - * - * - [databind#994] - */ -public class EmptyArrayAsNullTest extends BaseMapTest -{ - private final ObjectMapper MAPPER = new ObjectMapper(); - private final ObjectReader DEFAULT_READER = MAPPER.reader(); - private final ObjectReader READER_WITH_ARRAYS = DEFAULT_READER - .with(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT); - - static class Bean { - public String a = "foo"; - } - - final static String EMPTY_ARRAY = " [\n]"; - - /* - /********************************************************** - /* Test methods, settings - /********************************************************** - */ - - public void testSettings() { - assertFalse(MAPPER.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)); - assertFalse(DEFAULT_READER.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)); - assertTrue(READER_WITH_ARRAYS.isEnabled(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)); - } - - /* - /********************************************************** - /* Test methods, POJOs - /********************************************************** - */ - - // [databind#540] - public void testPOJOFromEmptyArray() throws Exception - { - // first, verify default settings which do not accept empty Array - try { - DEFAULT_READER.forType(Bean.class) - .readValue(EMPTY_ARRAY); - fail("Should not accept Empty Array for POJO by default"); - } catch (JsonMappingException e) { - verifyException(e, "START_ARRAY token"); - assertValidLocation(e.getLocation()); - } - - // should be ok to enable dynamically: - Bean result = READER_WITH_ARRAYS.forType(Bean.class) - .readValue(EMPTY_ARRAY); - assertNull(result); - } - - /* - /********************************************************** - /* Test methods, Maps - /********************************************************** - */ - - public void testMapFromEmptyArray() throws Exception - { - // first, verify default settings which do not accept empty Array - try { - DEFAULT_READER.forType(Map.class) - .readValue(EMPTY_ARRAY); - fail("Should not accept Empty Array for Map by default"); - } catch (JsonMappingException e) { - verifyException(e, "START_ARRAY token"); - } - // should be ok to enable dynamically: - Map<?,?> result = READER_WITH_ARRAYS.forType(Map.class) - .readValue(EMPTY_ARRAY); - assertNull(result); - } - - public void testEnumMapFromEmptyArray() throws Exception - { - - EnumMap<?,?> result2 = READER_WITH_ARRAYS.forType(new TypeReference<EnumMap<ABC,String>>() { }) - .readValue(EMPTY_ARRAY); - assertNull(result2); - } - - /* - /********************************************************** - /* Test methods, primitives/wrappers - /********************************************************** - */ - - public void testWrapperFromEmptyArray() throws Exception - { - _testNullWrapper(Boolean.class); - _testNullWrapper(Byte.class); - _testNullWrapper(Character.class); - _testNullWrapper(Short.class); - _testNullWrapper(Integer.class); - _testNullWrapper(Long.class); - _testNullWrapper(Float.class); - _testNullWrapper(Double.class); - } - - /* - /********************************************************** - /* Test methods, other - /********************************************************** - */ - - public void testNullStringFromEmptyArray() throws Exception { - _testNullWrapper(String.class); - } - - public void testNullEnumFromEmptyArray() throws Exception { - _testNullWrapper(ABC.class); - } - - public void testStdJdkTypesFromEmptyArray() throws Exception - { - _testNullWrapper(BigInteger.class); - _testNullWrapper(BigDecimal.class); - - _testNullWrapper(UUID.class); - - _testNullWrapper(Date.class); - _testNullWrapper(Calendar.class); - } - - /* - /********************************************************** - /* Helper methods - /********************************************************** - */ - - private void _testNullWrapper(Class<?> cls) throws Exception - { - Object result = READER_WITH_ARRAYS.forType(cls).readValue(EMPTY_ARRAY); - assertNull(result); - } -} diff --git a/src/test/java/com/fasterxml/jackson/databind/struct/UnwrapSingleArrayScalarsTest.java b/src/test/java/com/fasterxml/jackson/databind/struct/UnwrapSingleArrayScalarsTest.java index a13988e90..cb06cd559 100644 --- a/src/test/java/com/fasterxml/jackson/databind/struct/UnwrapSingleArrayScalarsTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/struct/UnwrapSingleArrayScalarsTest.java @@ -80,7 +80,7 @@ public class UnwrapSingleArrayScalarsTest extends BaseMapTest .readValue("{\"v\":[3]}"); fail("Did not throw exception when reading a value from a single value array with the UNWRAP_SINGLE_VALUE_ARRAYS feature disabled"); } catch (MismatchedInputException e) { - verifyException(e, "Cannot deserialize instance of `int`"); + verifyException(e, "Cannot deserialize value of type `int`"); } ObjectReader r = UNWRAPPING_READER.forType(IntBean.class); @@ -117,7 +117,7 @@ public class UnwrapSingleArrayScalarsTest extends BaseMapTest noUnwrapR.readValue("{\"v\":[3]}"); fail("Did not throw exception when reading a value from a single value array"); } catch (MismatchedInputException e) { - verifyException(e, "Cannot deserialize instance of `long`"); + verifyException(e, "Cannot deserialize value of type `long`"); } LongBean result = unwrapR.readValue("{\"v\":[3]}"); @@ -154,7 +154,7 @@ public class UnwrapSingleArrayScalarsTest extends BaseMapTest noUnwrapR.readValue("{\"v\":[" + value + "]}"); fail("Did not throw exception when reading a value from a single value array"); } catch (MismatchedInputException e) { - verifyException(e, "Cannot deserialize instance of `double`"); + verifyException(e, "Cannot deserialize value of type `double`"); } DoubleBean result = unwrapR.readValue("{\"v\":[" + value + "]}"); @@ -359,8 +359,7 @@ public class UnwrapSingleArrayScalarsTest extends BaseMapTest NO_UNWRAPPING_READER.readValue("[\""+value+"\"]", String.class); fail("Exception not thrown when attempting to unwrap a single value 'String' array into a simple String"); } catch (MismatchedInputException exp) { - verifyException(exp, "Cannot deserialize"); - verifyException(exp, "out of START_ARRAY"); + _verifyNoDeserFromArray(exp); } try { @@ -385,8 +384,7 @@ public class UnwrapSingleArrayScalarsTest extends BaseMapTest r.readValue("[" + value.toString() + "]"); fail("Exception was not thrown when attempting to read a single value array of BigDecimal when UNWRAP_SINGLE_VALUE_ARRAYS feature is disabled"); } catch (MismatchedInputException exp) { - verifyException(exp, "Cannot deserialize"); - verifyException(exp, "out of START_ARRAY"); + _verifyNoDeserFromArray(exp); } r = UNWRAPPING_READER.forType(BigDecimal.class); @@ -411,8 +409,7 @@ public class UnwrapSingleArrayScalarsTest extends BaseMapTest NO_UNWRAPPING_READER.readValue("[" + value.toString() + "]", BigInteger.class); fail("Exception was not thrown when attempting to read a single value array of BigInteger when UNWRAP_SINGLE_VALUE_ARRAYS feature is disabled"); } catch (MismatchedInputException exp) { - verifyException(exp, "Cannot deserialize"); - verifyException(exp, "out of START_ARRAY"); + _verifyNoDeserFromArray(exp); } result = UNWRAPPING_READER.readValue("[" + value.toString() + "]", BigInteger.class); @@ -438,7 +435,7 @@ public class UnwrapSingleArrayScalarsTest extends BaseMapTest .readValue("[" + quote(String.class.getName()) + "]"); fail("Did not throw exception when UNWRAP_SINGLE_VALUE_ARRAYS feature was disabled and attempted to read a Class array containing one element"); } catch (MismatchedInputException e) { - verifyException(e, "out of START_ARRAY token"); + _verifyNoDeserFromArray(e); } _verifyMultiValueArrayFail("[" + quote(Object.class.getName()) + "," + quote(Object.class.getName()) +"]", @@ -456,7 +453,7 @@ public class UnwrapSingleArrayScalarsTest extends BaseMapTest .readValue("[\""+value.toString()+"\"]"); fail("Did not throw exception for single value array when UNWRAP_SINGLE_VALUE_ARRAYS is disabled"); } catch (MismatchedInputException e) { - verifyException(e, "out of START_ARRAY token"); + _verifyNoDeserFromArray(e); } _verifyMultiValueArrayFail("[\""+value.toString()+"\",\""+value.toString()+"\"]", URI.class); @@ -471,7 +468,7 @@ public class UnwrapSingleArrayScalarsTest extends BaseMapTest .readValue("[" + quote(uuidStr) + "]"); fail("Exception was not thrown as expected"); } catch (MismatchedInputException e) { - verifyException(e, "out of START_ARRAY token"); + _verifyNoDeserFromArray(e); } assertEquals(uuid, UNWRAPPING_READER.forType(UUID.class) @@ -485,6 +482,12 @@ public class UnwrapSingleArrayScalarsTest extends BaseMapTest /********************************************************** */ + private void _verifyNoDeserFromArray(Exception e) { + verifyException(e, "Cannot deserialize"); + verifyException(e, "from Array value"); + verifyException(e, "JsonToken.START_ARRAY"); + } + private void _verifyMultiValueArrayFail(String input, Class<?> type) throws IOException { try { UNWRAPPING_READER.forType(type).readValue(input); diff --git a/src/test/java/com/fasterxml/jackson/databind/type/TestJavaType.java b/src/test/java/com/fasterxml/jackson/databind/type/TestJavaType.java index def53ce11..5487a4c7f 100644 --- a/src/test/java/com/fasterxml/jackson/databind/type/TestJavaType.java +++ b/src/test/java/com/fasterxml/jackson/databind/type/TestJavaType.java @@ -280,7 +280,12 @@ public class TestJavaType assertTrue(t.hasContentType()); assertEquals(Long.class, t.getContentType().getRawClass()); - // 26-Mar-2020, tatu: [databind#2019] suggest this should be 1... -// assertEquals(1, t.containedTypeCount()); + // 26-Mar-2020, tatu: [databind#2019] made this work + assertEquals(1, t.containedTypeCount()); + TypeBindings bindings = t.getBindings(); + assertEquals(1, bindings.size()); + assertEquals(refdType, bindings.getBoundType(0)); + // Should we even verify this or not? + assertEquals("V", bindings.getBoundName(0)); } } diff --git a/src/test/java/com/fasterxml/jackson/databind/type/TestTypeFactory.java b/src/test/java/com/fasterxml/jackson/databind/type/TestTypeFactory.java index e78c15d1a..dc326d5d1 100644 --- a/src/test/java/com/fasterxml/jackson/databind/type/TestTypeFactory.java +++ b/src/test/java/com/fasterxml/jackson/databind/type/TestTypeFactory.java @@ -160,7 +160,7 @@ public class TestTypeFactory { TypeFactory tf = TypeFactory.defaultInstance(); // first, simple class based - JavaType t = tf.constructParametrizedType(ArrayList.class, Collection.class, String.class); // ArrayList<String> + final JavaType t = tf.constructParametrizedType(ArrayList.class, Collection.class, String.class); // ArrayList<String> assertEquals(CollectionType.class, t.getClass()); JavaType strC = tf.constructType(String.class); assertEquals(1, t.containedTypeCount()); @@ -176,6 +176,13 @@ public class TestTypeFactory assertEquals(t, t2.containedType(1)); assertNull(t2.containedType(2)); + // [databind#921]: using type bindings + JavaType t3 = tf.constructParametricType(HashSet.class, t.getBindings()); // HashSet<String> + assertEquals(CollectionType.class, t3.getClass()); + assertEquals(1, t3.containedTypeCount()); + assertEquals(strC, t3.containedType(0)); + assertNull(t3.containedType(1)); + // and then custom generic type as well JavaType custom = tf.constructParametrizedType(SingleArgGeneric.class, SingleArgGeneric.class, String.class); @@ -184,10 +191,24 @@ public class TestTypeFactory assertEquals(strC, custom.containedType(0)); assertNull(custom.containedType(1)); + // and then custom generic type from TypeBindings ([databind#921]) + JavaType custom2 = tf.constructParametricType(SingleArgGeneric.class, t.getBindings()); + assertEquals(SimpleType.class, custom2.getClass()); + assertEquals(1, custom2.containedTypeCount()); + assertEquals(strC, custom2.containedType(0)); + assertNull(custom2.containedType(1)); + // should also be able to access variable name: assertEquals("X", custom.containedTypeName(0)); + } - // And finally, ensure that we can't create invalid combinations + @SuppressWarnings("deprecation") + public void testInvalidParametricTypes() + { + final TypeFactory tf = TypeFactory.defaultInstance(); + final JavaType strC = tf.constructType(String.class); + + // ensure that we can't create invalid combinations try { // Maps must take 2 type parameters, not just one tf.constructParametrizedType(Map.class, Map.class, strC); @@ -202,7 +223,7 @@ public class TestTypeFactory verifyException(e, "Cannot create TypeBindings for class "); } } - + /** * Test for checking that canonical name handling works ok */ diff --git a/src/test/java/com/fasterxml/jackson/failing/BuilderWithIgnored1214Test.java b/src/test/java/com/fasterxml/jackson/failing/BuilderWithIgnored1214Test.java deleted file mode 100644 index 54c6d6a76..000000000 --- a/src/test/java/com/fasterxml/jackson/failing/BuilderWithIgnored1214Test.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.fasterxml.jackson.failing; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -public class BuilderWithIgnored1214Test extends BaseMapTest -{ - @JsonDeserialize(builder = TestObject1214.Builder.class) - @JsonIgnoreProperties(ignoreUnknown = true) - static class TestObject1214 { - final String property1; - - private TestObject1214(Builder builder) { - property1 = builder.property1; - } - - public static Builder builder() { - return new Builder(); - } - - public String getProperty1() { - return property1; - } - - static class Builder { - - private String property1; - - public Builder withProperty1(String p1) { - property1 = p1; - return this; - } - - public TestObject1214 build() { - return new TestObject1214(this); - } - } - } - - public void testUnknown1214() throws Exception - { - ObjectMapper mapper = new ObjectMapper(); - TestObject1214 value = mapper.readValue(aposToQuotes - ("{'property1':'a', 'property2':'b'}"), - TestObject1214.class); - assertEquals("a", value.property1); - } -} diff --git a/src/test/java/com/fasterxml/jackson/failing/JDKNumberDeser2644Test.java b/src/test/java/com/fasterxml/jackson/failing/JDKNumberDeser2644Test.java deleted file mode 100644 index d92300a35..000000000 --- a/src/test/java/com/fasterxml/jackson/failing/JDKNumberDeser2644Test.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.fasterxml.jackson.failing; - -import java.math.BigDecimal; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import com.fasterxml.jackson.databind.*; - -// Tests for -// -// [databind#2644] -// [databind#2785] -public class JDKNumberDeser2644Test extends BaseMapTest -{ - // [databind#2644] - static class NodeRoot2644 { - public String type; - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type") - @JsonSubTypes(value = { - @JsonSubTypes.Type(value = NodeParent2644.class, name = "NodeParent") - }) - public Node2644 node; - } - - public static class NodeParent2644 extends Node2644 { } - - public static abstract class Node2644 { - @JsonProperty("amount") - BigDecimal val; - - public BigDecimal getVal() { - return val; - } - - public void setVal(BigDecimal val) { - this.val = val; - } - } - - // [databind#2785] - static class BigDecimalHolder2785 { - public BigDecimal value; - } - - static class NestedBigDecimalHolder2785 { - @JsonUnwrapped - public BigDecimalHolder2785 holder; - } - - // [databind#2644] - public void testBigDecimalSubtypes() throws Exception - { - ObjectMapper mapper = newJsonMapper(); - - // NOTE: uncommenting this does work around the issue: -// mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); - mapper.registerSubtypes(NodeParent2644.class); - - NodeRoot2644 root = mapper.readValue( - "{\"type\": \"NodeParent\",\"node\": {\"amount\": 9999999999999999.99} }", - NodeRoot2644.class - ); - - assertEquals(new BigDecimal("9999999999999999.99"), root.node.getVal()); - } - - // [databind#2785] - - public void testBigDecimalUnwrapped() throws Exception - { - final ObjectMapper mapper = newJsonMapper(); - // mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); - final String JSON = "{\"value\": 5.00}"; - - // first simple working case: - BigDecimalHolder2785 holder = mapper.readValue(JSON, BigDecimalHolder2785.class); - assertEquals(new BigDecimal("5.00"), holder.value); - - // and then one that doesn't - NestedBigDecimalHolder2785 result = mapper.readValue(JSON, NestedBigDecimalHolder2785.class); - assertEquals(new BigDecimal("5.00"), result.holder.value); - } -} diff --git a/src/test/java/com/fasterxml/jackson/failing/NumberNodes1770Test.java b/src/test/java/com/fasterxml/jackson/failing/NumberNodes1770Test.java index 192feb4ec..a2e18f0ff 100644 --- a/src/test/java/com/fasterxml/jackson/failing/NumberNodes1770Test.java +++ b/src/test/java/com/fasterxml/jackson/failing/NumberNodes1770Test.java @@ -19,7 +19,7 @@ public class NumberNodes1770Test extends BaseMapTest final JsonNode jsonNode = MAPPER.reader() .with(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) .readTree("7976931348623157e309"); - assertTrue(jsonNode.isBigDecimal()); + assertTrue("Expected DecimalNode, got: "+jsonNode.getClass().getName()+": "+jsonNode, jsonNode.isBigDecimal()); // the following fails with NumberFormatException, because jsonNode is a DoubleNode with a value of POSITIVE_INFINITY // Assert.assertTrue(jsonNode.decimalValue().compareTo(new BigDecimal("7976931348623157e309")) == 0); } diff --git a/src/test/java/com/fasterxml/jackson/failing/ParsingContextExtTypeId2747Test.java b/src/test/java/com/fasterxml/jackson/failing/ParsingContextExtTypeId2747Test.java new file mode 100644 index 000000000..3bc900f5a --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/failing/ParsingContextExtTypeId2747Test.java @@ -0,0 +1,65 @@ +package com.fasterxml.jackson.failing; + +import java.io.IOException; + +import com.fasterxml.jackson.annotation.*; + +import com.fasterxml.jackson.core.*; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +public class ParsingContextExtTypeId2747Test extends BaseMapTest +{ + static class Wrapper { + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", + include = JsonTypeInfo.As.EXTERNAL_PROPERTY) + public Tag wrapped; + + public String type; + } + + @JsonSubTypes(@JsonSubTypes.Type(Location.class)) + interface Tag {} + + @JsonTypeName("location") + @JsonDeserialize(using = LocationDeserializer.class) + static class Location implements Tag + { + String value; + + protected Location() { } + Location(String v) { value = v; } + } + + static class LocationDeserializer extends JsonDeserializer<Location> + { + @Override + public Location deserialize(JsonParser p, DeserializationContext ctxt) throws IOException + { + p.skipChildren(); + return new Location(getCurrentLocationAsString(p)); + } + } + + static String getCurrentLocationAsString(JsonParser p) + { + // This suffices to give actual path + return p.getParsingContext().pathAsPointer().toString(); + } + + // [databind#2747] + public void testLocationAccessWithExtTypeId() throws Exception + { + ObjectReader objectReader = newJsonMapper().readerFor(Wrapper.class); + + Wrapper wrapper = objectReader.readValue("{" + + "\"type\":\"location\"," + + "\"wrapped\": 1" + + "}"); + // expecting wrapper.wrapped.value == "wrapped" but is "wrapped[1]" + // due to way `ExternalTypeHandler` exposes value as if "wrapper-array" was used for + // type id, value + assertEquals("/wrapped", ((Location) wrapper.wrapped).value); + } +} diff --git a/src/test/java/com/fasterxml/jackson/failing/TestUnknownProperty426.java b/src/test/java/com/fasterxml/jackson/failing/TestUnknownProperty426.java deleted file mode 100644 index 09b3226b7..000000000 --- a/src/test/java/com/fasterxml/jackson/failing/TestUnknownProperty426.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.fasterxml.jackson.failing; - -import com.fasterxml.jackson.annotation.*; - -import com.fasterxml.jackson.databind.*; - -/** - * Unit tests for checking handling of unknown properties - */ -public class TestUnknownProperty426 extends BaseMapTest -{ - // For [databind#426] - @JsonIgnoreProperties({ "userId" }) - static class User { - public String firstName; - Integer userId; - - public void setUserId(CharSequence id) { - // 21-Dec-2015, tatu: With a fix in 2.7, use of String would not - // trigger the problem, so use CharSequence... - setUserId(Integer.valueOf(id.toString())); - } - - public Integer getUserId() { - return userId; - } - - public void setUserId(Integer v) { - this.userId = v; - } - } - - /* - /********************************************************** - /* Test methods - /********************************************************** - */ - - private final ObjectMapper MAPPER = new ObjectMapper(); - - // Aside from [databind#426], also relevant: [databind#1044]? - public void testIssue426() throws Exception - { - final String JSON = aposToQuotes("{'userId': 9, 'firstName': 'Mike' }"); - User result = MAPPER.readerFor(User.class).readValue(JSON); - assertNotNull(result); - assertEquals("Mike", result.firstName); - } -} - diff --git a/src/test/java/com/fasterxml/jackson/failing/VoidProperties2675Test.java b/src/test/java/com/fasterxml/jackson/failing/VoidProperties2675Test.java deleted file mode 100644 index c7727eb48..000000000 --- a/src/test/java/com/fasterxml/jackson/failing/VoidProperties2675Test.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.fasterxml.jackson.failing; - -import com.fasterxml.jackson.databind.*; - -public class VoidProperties2675Test extends BaseMapTest -{ - static class VoidBean { - public Void getValue() { return null; } - } - - /* - /********************************************************************** - /* Test methods - /********************************************************************** - */ - - private final ObjectMapper MAPPER = new ObjectMapper(); - - public void testVoidBean() throws Exception { - final String EXP = "{\"value\":null}"; - assertEquals(EXP, MAPPER.writeValueAsString(new VoidBean())); - VoidBean result = MAPPER.readValue(EXP, VoidBean.class); - assertNotNull(result); - } -} |