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 bdcf988bb2..280fac56d3 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 @@ -459,9 +459,30 @@ value, rewrapCtorProblem(ctxt, t) } } + // 13-Dec-2020, ckozak: Unlike other types, BigDecimal values may be represented + // with less precision as doubles. When written to a TokenBuffer for polymorphic + // deserialization the most specific type is recorded, though a less precise + // floating point value may be needed. + if(_fromDoubleCreator != null && canConvertToDouble(value)) { + Object arg = value.doubleValue(); + try { + return _fromDoubleCreator.call1(arg); + } catch (Throwable t0) { + return ctxt.handleInstantiationProblem(_fromDoubleCreator.getDeclaringClass(), + arg, rewrapCtorProblem(ctxt, t0)); + } + } + return super.createFromBigDecimal(ctxt, value); } + // BigDecimal cannot represent special values NaN, positive infinity, or negative infinity. + // When the value cannot be represented as a double, positive or negative infinity is returned. + static boolean canConvertToDouble(BigDecimal value) { + double doubleValue = value.doubleValue(); + return !Double.isInfinite(doubleValue); + } + @Override public Object createFromBoolean(DeserializationContext ctxt, boolean value) throws IOException { diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiatorTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiatorTest.java new file mode 100644 index 0000000000..02d2cd626b --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiatorTest.java @@ -0,0 +1,20 @@ +package com.fasterxml.jackson.databind.deser.std; + +import com.fasterxml.jackson.databind.BaseMapTest; + +import java.math.BigDecimal; + +public class StdValueInstantiatorTest extends BaseMapTest { + + public void testDoubleValidation_valid() { + assertTrue(StdValueInstantiator.canConvertToDouble(BigDecimal.ZERO)); + assertTrue(StdValueInstantiator.canConvertToDouble(BigDecimal.ONE)); + assertTrue(StdValueInstantiator.canConvertToDouble(BigDecimal.TEN)); + assertTrue(StdValueInstantiator.canConvertToDouble(BigDecimal.valueOf(-1.5D))); + } + + public void testDoubleValidation_invalid() { + BigDecimal value = BigDecimal.valueOf(Double.MAX_VALUE).add(BigDecimal.valueOf(Double.MAX_VALUE)); + assertFalse(StdValueInstantiator.canConvertToDouble(value)); + } +} \ No newline at end of file diff --git a/src/test/java/com/fasterxml/jackson/databind/jsontype/TestDoubleJsonCreator.java b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestDoubleJsonCreator.java new file mode 100644 index 0000000000..b1e41fc156 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/jsontype/TestDoubleJsonCreator.java @@ -0,0 +1,229 @@ +package com.fasterxml.jackson.databind.jsontype; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.BaseMapTest; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class TestDoubleJsonCreator extends BaseMapTest { + + public static final class UnionExample { + private final Base value; + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + private UnionExample(Base value) { + this.value = value; + } + + @JsonValue + private Base getValue() { + return value; + } + + public static UnionExample double_(AliasDouble value) { + return new UnionExample(new DoubleWrapper(value)); + } + + public T accept(Visitor visitor) { + return value.accept(visitor); + } + + @Override + public boolean equals(Object other) { + return this == other || (other instanceof UnionExample && equalTo((UnionExample) other)); + } + + private boolean equalTo(UnionExample other) { + return this.value.equals(other.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.value); + } + + @Override + public String toString() { + return "UnionExample{value: " + value + '}'; + } + + public interface Visitor { + T visitDouble(AliasDouble value); + + T visitUnknown(String unknownType); + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true, defaultImpl = UnknownWrapper.class) + @JsonSubTypes(@JsonSubTypes.Type(UnionExample.DoubleWrapper.class)) + @JsonIgnoreProperties(ignoreUnknown = true) + private interface Base { + T accept(Visitor visitor); + } + + @JsonTypeName("double") + private static final class DoubleWrapper implements Base { + private final AliasDouble value; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + private DoubleWrapper(@JsonSetter("double") AliasDouble value) { + Objects.requireNonNull(value, "double cannot be null"); + this.value = value; + } + + @JsonProperty("double") + private AliasDouble getValue() { + return value; + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitDouble(value); + } + + @Override + public boolean equals(Object other) { + return this == other || (other instanceof DoubleWrapper && equalTo((DoubleWrapper) other)); + } + + private boolean equalTo(DoubleWrapper other) { + return this.value.equals(other.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.value); + } + + @Override + public String toString() { + return "DoubleWrapper{value: " + value + '}'; + } + } + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true) + private static final class UnknownWrapper implements Base { + private final String type; + + private final Map value; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + private UnknownWrapper(@JsonProperty("type") String type) { + this(type, new HashMap()); + } + + private UnknownWrapper(String type, Map value) { + Objects.requireNonNull(type, "type cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + this.type = type; + this.value = value; + } + + @JsonProperty + private String getType() { + return type; + } + + @JsonAnyGetter + private Map getValue() { + return value; + } + + @JsonAnySetter + private void put(String key, Object val) { + value.put(key, val); + } + + @Override + public T accept(Visitor visitor) { + return visitor.visitUnknown(type); + } + + @Override + public boolean equals(Object other) { + return this == other || (other instanceof UnknownWrapper && equalTo((UnknownWrapper) other)); + } + + private boolean equalTo(UnknownWrapper other) { + return this.type.equals(other.type) && this.value.equals(other.value); + } + + @Override + public int hashCode() { + return Objects.hash(this.type, this.value); + } + + @Override + public String toString() { + return "UnknownWrapper{type: " + type + ", value: " + value + '}'; + } + } + } + + public static final class AliasDouble { + private final double value; + + private AliasDouble(double value) { + this.value = value; + } + + @JsonValue + public double get() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @Override + public boolean equals(Object other) { + return this == other + || (other instanceof AliasDouble + && Double.doubleToLongBits(this.value) == Double.doubleToLongBits(((AliasDouble) other).value)); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static AliasDouble of(double value) { + return new AliasDouble(value); + } + } + + public void testDeserializationTypeFieldLast() throws IOException { + UnionExample expected = UnionExample.double_(AliasDouble.of(2.0D)); + UnionExample actual = objectMapper().readValue( + a2q("{'double': 2.0,'type':'double'}"), + new TypeReference() {}); + assertEquals(expected, actual); + } + + public void testDeserializationTypeFieldFirst() throws IOException { + UnionExample expected = UnionExample.double_(AliasDouble.of(2.0D)); + UnionExample actual = objectMapper().readValue( + a2q("{'type':'double','double': 2.0}"), + new TypeReference() {}); + assertEquals(expected, actual); + } +}