Skip to content

Commit

Permalink
Fix failing double JsonCreators in jackson 2.12
Browse files Browse the repository at this point in the history
The addition of Big numeric types, BigInteger and BigDecimal,
prevents some tokens from being handled by existing `double`
deserializers.

This path occurs when data is buffered into a TokenBuffer while
deserializing polymorphic types if the type name is not the first
field received.
  • Loading branch information
carterkozak committed Dec 10, 2020
1 parent 9dab814 commit 747258b
Show file tree
Hide file tree
Showing 3 changed files with 298 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public class StdValueInstantiator
{
private static final long serialVersionUID = 1L;

private static final BigInteger BIG_INTEGER_MAX_LONG = BigInteger.valueOf(Long.MAX_VALUE);
private static final BigInteger BIG_INTEGER_MIN_LONG = BigInteger.valueOf(Long.MIN_VALUE);

/**
* Type of values that are instantiated; used
* for error reporting purposes.
Expand Down Expand Up @@ -418,9 +421,25 @@ value, rewrapCtorProblem(ctxt, t)
}
}

if (_fromLongCreator != null && isValidLong(value)) {
Object arg = value.longValue();
try {
return _fromLongCreator.call1(arg);
} catch (Throwable t0) {
return ctxt.handleInstantiationProblem(_fromLongCreator.getDeclaringClass(),
arg,
rewrapCtorProblem(ctxt, t0)
);
}
}
return super.createFromBigInteger(ctxt, value);
}

// Validate teh BigInteger may be represented by a 64-bit integer
static boolean isValidLong(BigInteger value) {
return BIG_INTEGER_MIN_LONG.compareTo(value) <= 0 && BIG_INTEGER_MAX_LONG.compareTo(value) >= 0;
}

@Override
public Object createFromDouble(DeserializationContext ctxt, double value) throws IOException
{
Expand Down Expand Up @@ -460,9 +479,26 @@ value, rewrapCtorProblem(ctxt, t)
}
}

if(_fromDoubleCreator != null && isValidDouble(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 isValidDouble(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,33 @@
package com.fasterxml.jackson.databind.deser.std;

import com.fasterxml.jackson.databind.BaseMapTest;

import java.math.BigDecimal;
import java.math.BigInteger;

public class StdValueInstantiatorTest extends BaseMapTest {

public void testLongValidation_valid() {
assertTrue(StdValueInstantiator.isValidLong(BigInteger.TEN));
assertTrue(StdValueInstantiator.isValidLong(BigInteger.ZERO));
assertTrue(StdValueInstantiator.isValidLong(BigInteger.valueOf(Long.MIN_VALUE)));
assertTrue(StdValueInstantiator.isValidLong(BigInteger.valueOf(Long.MAX_VALUE)));
}

public void testLongValidation_invalid() {
assertFalse(StdValueInstantiator.isValidLong(BigInteger.valueOf(Long.MIN_VALUE).subtract(BigInteger.ONE)));
assertFalse(StdValueInstantiator.isValidLong(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE)));
}

public void testDoubleValidation_valid() {
assertTrue(StdValueInstantiator.isValidDouble(BigDecimal.ZERO));
assertTrue(StdValueInstantiator.isValidDouble(BigDecimal.ONE));
assertTrue(StdValueInstantiator.isValidDouble(BigDecimal.TEN));
assertTrue(StdValueInstantiator.isValidDouble(BigDecimal.valueOf(-1.5D)));
}

public void testDoubleValidation_invalid() {
BigDecimal value = BigDecimal.valueOf(Double.MAX_VALUE).add(BigDecimal.valueOf(Double.MAX_VALUE));
assertFalse(StdValueInstantiator.isValidDouble(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);
}

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);
}
}

0 comments on commit 747258b

Please sign in to comment.