Skip to content

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

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

Closed
anjaliguptaz opened this issue Mar 20, 2017 · 13 comments
Closed
Labels
node-config Related to JSTEP-7 Node config/feature

Comments

@anjaliguptaz
Copy link

I was using Jackson version 2.6 and came across the issue where the 0.0 double is thrown away during deserialization (#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 .

@cowtowncoder
Copy link
Member

@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
Copy link
Member

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

@hylkevds
Copy link

hylkevds commented Jan 22, 2019

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
Copy link

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

@cowtowncoder cowtowncoder reopened this Jan 24, 2019
@cowtowncoder
Copy link
Member

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

@GedMarc
Copy link

GedMarc commented Jan 24, 2019

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

@cowtowncoder
Copy link
Member

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

@hylkevds
Copy link

Setting a NodeFactory with the excactBigDecimals flag set works 👍

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.

@cowtowncoder
Copy link
Member

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

hylkevds added a commit to FraunhoferIOSB/FROST-Server that referenced this issue Mar 9, 2019
@cowtowncoder cowtowncoder added 2.12 and removed 2.10 labels Apr 12, 2020
@cowtowncoder cowtowncoder added 2.13 node-config Related to JSTEP-7 Node config/feature and removed 2.12 labels Oct 27, 2020
@cowtowncoder
Copy link
Member

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

@dmytrokarimov
Copy link

dmytrokarimov commented Jan 27, 2021

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

@nicolasmafraintelipost
Copy link

nicolasmafraintelipost commented Aug 17, 2022

I have a more general case:
image

Solution:
image

This works for "0.0" case too.

@cowtowncoder cowtowncoder changed the title Double value 0.0 getting deserialized as 0 Double value 0.0 getting deserialized as 0 with Tree Model (JsonNode) Aug 17, 2022
@cowtowncoder cowtowncoder removed the 2.14 label Mar 2, 2024
@cowtowncoder
Copy link
Member

Not quite clear what the ask is; closing, may be re-filed with clear explanation & considering all current (2.19) configurability via JsonNodeFeature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
node-config Related to JSTEP-7 Node config/feature
Projects
None yet
Development

No branches or pull requests

6 participants