jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

Double value 0.0 getting deserialized as 0 with Tree Model (`JsonNode`)

Open anjaliguptaz opened this issue 7 years ago • 12 comments

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 avatar Mar 20 '17 20:03 anjaliguptaz

@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.

cowtowncoder avatar Mar 20 '17 23:03 cowtowncoder

Can not reproduce; may be reopened with a unit test or similar reproduction (on 2.8.7 or 2.9.0.pr2)

cowtowncoder avatar Mar 24 '17 04:03 cowtowncoder

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...

hylkevds avatar Jan 22 '19 09:01 hylkevds

@cowtowncoder : Should I create a new issue, or can you re-open this one?

hylkevds avatar Jan 23 '19 09:01 hylkevds

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....

cowtowncoder avatar Jan 24 '19 22:01 cowtowncoder

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

GedMarc avatar Jan 24 '19 22:01 GedMarc

@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 JsonNodes. Same is not true for general BigDecimals, 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.

cowtowncoder avatar Jan 25 '19 04:01 cowtowncoder

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 avatar Jan 25 '19 09:01 hylkevds

@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"

cowtowncoder avatar Jan 26 '19 03:01 cowtowncoder

Related to possible "node-config", see JSTEP-3 (https://github.com/FasterXML/jackson-future-ideas/wiki/JSTEP-3), tagging as such.

cowtowncoder avatar Oct 27 '20 04:10 cowtowncoder

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

dmytrokarimov avatar Jan 27 '21 18:01 dmytrokarimov

I have a more general case: image

Solution: image

This works for "0.0" case too.

nicolasmafraintelipost avatar Aug 17 '22 20:08 nicolasmafraintelipost