jackson-databind
jackson-databind copied to clipboard
Double value 0.0 getting deserialized as 0 with Tree Model (`JsonNode`)
I was using Jackson version 2.6 and came across the issue where the 0.0 double is thrown away during deserialization (https://github.com/fasterxml/jackson-databind/issues/849 ) So i upgraded to Jackson version 2.7 .Now the double value is there but 0.0 is getting deserialized as 0. Any help on how to solve this is much appreciated .
@anjaliguptaz Please provide code example of what exactly you are doing: description does not really give many details (POJO or JsonNode
or streaming generation?). Ideally unit test.
Can not reproduce; may be reopened with a unit test or similar reproduction (on 2.8.7 or 2.9.0.pr2)
I just ran into the same issue. There is a difference in behaviour between readTree and readValue. readValue behaves as expected, readTree returns the incorrect 0. Demo code, version 2.9.8:
ObjectMapper mapper = new ObjectMapper()
.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
JsonNode treeMode = mapper.readTree("{\"value\": 0.0}");
Object value = treeMode.get("value").decimalValue();
System.out.println("value: " + value.getClass().getName() + " " + value.toString());
TypeReference mapOfStringObject = new TypeReference<Map<String, Object>>() {};
Map<String, Object> map = mapper.readValue("{\"value\": 0.0}", mapOfStringObject);
value = map.get("value");
System.out.println("value: " + value.getClass().getName() + " " + value.toString());
Output:
value: java.math.BigDecimal 0
value: java.math.BigDecimal 0.0
The difference is in that JsonNodeFactory#numberNode(BigDecimal v)
does:
return v.compareTo(BigDecimal.ZERO) == 0 ? DecimalNode.ZERO
: DecimalNode.valueOf(v.stripTrailingZeros());
There seems to be a flag to suppress this behaviour, but I've not been able to find how to set this flag.
I'm not the original poster, so I can't re-open this issue...
@cowtowncoder : Should I create a new issue, or can you re-open this one?
Hmmh. Interesting. Ok, Not quite sure any more what the logic for comparison here is, but would seem like instead of DecimalNode.ZERO
it should just return v
....
I hope this makes sense on this one -- this weeks been tough
For decimal from BigDecimal what I think i've found is you need to set the scale/precision manually to go into decimal() with 0.0? So can either use .double(), or set the precision and scale on big decimal before going into decimal()
Got a quick example below where I think it shows how it all comes together and difference of .double() vs .decimal()
ObjectMapper mapper = new ObjectMapper()
.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
System.out.println(BigDecimal.ZERO.compareTo(BigDecimal.valueOf(0.0)) == 0); //true
System.out.println(BigDecimal.ZERO.compareTo(BigDecimal.valueOf(0)) == 0); //true
BigDecimal a = new BigDecimal("0.00");
BigDecimal b = new BigDecimal("0.0");
BigDecimal c = new BigDecimal("0");
if(a.doubleValue()==BigDecimal.ZERO.doubleValue()) {
System.out.println("a equals");
}
if(b.doubleValue()==BigDecimal.ZERO.doubleValue()) {
System.out.println("b equals");
}
if(c.doubleValue()==BigDecimal.ZERO.doubleValue()) {
System.out.println("c equals");
}
JsonNode treeMode = mapper.readTree("{\"value\": 0.0}");
Object value = treeMode.get("value").doubleValue();
Object value2 = treeMode.get("value").decimalValue();
System.out.println("value: " + value.getClass().getName() + " " + value.toString());
System.out.println("value decimal : " + value2.getClass().getName() + " " + value2.toString());
TypeReference mapOfStringObject = new TypeReference<Map<String, Object>>() {};
Map<String, Object> map = mapper.readValue("{\"value\": 0.0}", mapOfStringObject);
value = map.get("value");
System.out.println("value: " + value.getClass().getName() + " " + value.toString());
@hylkevds Ok so there's bit more to the story, as you can see from JsonNodeFactory
constructors. I think what you want, to retain exact value without normalization, is to construct instance with argument true
and configure ObjectMapper
(or ObjectReader
with it).
Intent here is/was to make DecimalNode
value equality to be looser, meaning that -- for example -- source values of 1.00
and 1.0
and 1
would all equal. This is probably losing battle as number equality checks are a quagmire... but once upon a time enough users/developers felt that normalization makes sense by default.
Special case of BigDecimal.ZERO
is actually fix to make it also work for 0.0[0*]
values, but dropping of trailing zeroes is the default mechanism for BigDecimal
valued JsonNode
s.
Same is not true for general BigDecimal
s, which are handled without normalization.
For Jackson 3.0 we can improve this:
https://github.com/FasterXML/jackson-future-ideas/wiki/Jackson3-Changes---JsonNode
by making it configurable (I added one feature there). Or who knows, maybe some work can be brought back to 2.10
, even.
But right now I don't think I can actually change the default behavior, so configuring JsonNodeFactory
differently (or sub-classing) is the way to go.
Setting a NodeFactory with the excactBigDecimals flag set works :+1:
mapper.setNodeFactory(JsonNodeFactory.withExactBigDecimals(true))
I had been looking for a configuration option, and hadn't noticed the setNodeFactory method.
Having a configuration option would be nice. Especially since there are already two static instances created for both behaviours. In the meantime, I think it would help if some documentation is added to the DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS
javadoc, since that is the option that most people who care about precision will be looking at.
What the default behaviour is doesn't matter much, as long as it's configurable, but personally I think that having readTree
and readValue
behave differently is confusing.
@hylkevds Good idea wrt javadocs.
I can see how different behavior seems confusing, depending on one's POV. I have been struggling with the fact that Tree Model (JsonNode
) and POJO handling is -- and I think, should be -- very different, regarding various configuration options. Mostly in that JsonNode
should be faithful representation of what JSON is, with as little changes as possible.
But come to think of that aspect, default setting is actually against my own philosophical way, massaging values for easier comparison.
So I will need to make a note on that for "change[d] defaults for 3.0"
Related to possible "node-config", see JSTEP-3 (https://github.com/FasterXML/jackson-future-ideas/wiki/JSTEP-3), tagging as such.
simple test that show this problem
@Test
public void simpleTest() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
TestDto dto = new TestDto(BigDecimal.valueOf(14.99));
JsonNode actual = mapper.valueToTree(dto);
JsonNode expected = mapper.readTree(mapper.writeValueAsString(actual));
assertEquals(expected, actual);
}
@AllArgsConstructor
@Data
private static class TestDto {
BigDecimal sum;
}
result:
java.lang.AssertionError: expected: com.fasterxml.jackson.databind.node.ObjectNode<{"sum":14.99}> but was: com.fasterxml.jackson.databind.node.ObjectNode<{"sum":14.99}>
Expected :com.fasterxml.jackson.databind.node.ObjectNode<{"sum":14.99}>
Actual :com.fasterxml.jackson.databind.node.ObjectNode<{"sum":14.99}>
<Click to see difference>
at org.junit.Assert.fail(Assert.java:89)
at org.junit.Assert.failNotEquals(Assert.java:835)
at org.junit.Assert.assertEquals(Assert.java:120)
at org.junit.Assert.assertEquals(Assert.java:146)
I found that DoubleNode compared VS DecimalNode, but equals (in both classes) doesn't support comparing against other classes:
if (o == this) return true;
if (o == null) return false;
if (o instanceof DoubleNode) {
// We must account for NaNs: NaN does not equal NaN, therefore we have
// to use Double.compare().
final double otherValue = ((DoubleNode) o)._value;
return Double.compare(_value, otherValue) == 0;
}
return false;
so if one node DoubleNode and another DecimalNode - we will have equals = false
I have a more general case:
Solution:
This works for "0.0" case too.