Skip to content

Fix failing double JsonCreators in jackson 2.12.0 #2978

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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> T accept(Visitor<T> 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> {
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> T accept(Visitor<T> 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> T accept(Visitor<T> 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<String, Object> value;

@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
private UnknownWrapper(@JsonProperty("type") String type) {
this(type, new HashMap<String, Object>());
}

private UnknownWrapper(String type, Map<String, Object> 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<String, Object> getValue() {
return value;
}

@JsonAnySetter
private void put(String key, Object val) {
value.put(key, val);
}

@Override
public <T> T accept(Visitor<T> 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<UnionExample>() {});
assertEquals(expected, actual);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test passes on 2.11 and fails on 2.12 with:

Cannot construct instance of com.fasterxml.jackson.databind.jsontype.TestDoubleJsonCreator$AliasDouble (although at least one Creator exists): no BigDecimal/double/Double-argument constructor/factory method to deserialize from Number value (2.0)

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.fasterxml.jackson.databind.jsontype.TestDoubleJsonCreator$AliasDouble` (although at least one Creator exists): no BigDecimal/double/Double-argument constructor/factory method to deserialize from Number value (2.0)
 at [Source: (String)"{"double": 2.0,"type":"double"}"; line: 1, column: 23] (through reference chain: com.fasterxml.jackson.databind.jsontype.TestDoubleJsonCreator$UnionExample$DoubleWrapper["double"])

	at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
	at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1588)
	at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1213)
	at com.fasterxml.jackson.databind.deser.ValueInstantiator.createFromBigDecimal(ValueInstantiator.java:351)
	at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromBigDecimal(StdValueInstantiator.java:463)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromDouble(BeanDeserializerBase.java:1518)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:211)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:197)
	at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:542)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:565)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:449)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1390)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:362)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:230)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:197)
	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:135)
	at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:105)
	at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserializeWithType(AbstractDeserializer.java:263)
	at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:74)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1383)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:362)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:195)
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3548)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3531)
	at com.fasterxml.jackson.databind.jsontype.TestDoubleJsonCreator.testDeserializationTypeFieldLast(TestDoubleJsonCreator.java:216)
...


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<UnionExample>() {});
assertEquals(expected, actual);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this test passes on both versions.

}