Jackson-databind: Scale of deserialized BigDecimal are not always the same

Created on 25 Nov 2014  路  9Comments  路  Source: FasterXML/jackson-databind

Hi,

When I use the annotation @JsonUnwrapped to materialize a component bean, the scale of the BigDecimal deserialized is different. I've written a test case below to reproduce the issue.
Is there a way to control the scale of BigDecimal? Do I have to configure a custom deserializer?

(Test case reproduced with Jacskon 2.4.0 and 1.9.13 on Java 6)

Thanks,

Benoit

import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;

import java.math.BigDecimal;

public class BigDecimalScaleTest {

    private String json = "{" +
            "  \"name\": \"Ben\"," +
            "  \"first\": 12.00," +
            "  \"second\": 14.00" +
            "}";

    private ObjectMapper objectMapper = new ObjectMapper();

    @Test
    public void testScale() throws Exception {

        MyDomain myDomain = objectMapper.readValue(json, MyDomain.class);

        MyFlatDomain myFlatDomain = objectMapper.readValue(json, MyFlatDomain.class);

        Assert.assertEquals(myDomain.getName(), myFlatDomain.getName());
        Assert.assertEquals(myDomain.getFirst(), myFlatDomain.getFirst()); // 12.00 = 12.00
        Assert.assertEquals(myDomain.getInnerDomain().getSecond(), myFlatDomain.getSecond());
         // 14.0 != 14.00
    }

    private static class MyDomain {
        private String name;
        private BigDecimal first;
        @JsonUnwrapped
        private MyInnerDomain innerDomain;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public BigDecimal getFirst() {
            return first;
        }

        public void setFirst(BigDecimal first) {
            this.first = first;
        }

        public MyInnerDomain getInnerDomain() {
            return innerDomain;
        }

        public void setInnerDomain(MyInnerDomain innerDomain) {
            this.innerDomain = innerDomain;
        }
    }

    private static class MyInnerDomain {
        private BigDecimal second;

        public BigDecimal getSecond() {
            return second;
        }

        public void setSecond(BigDecimal second) {
            this.second = second;
        }
    }

    private static class MyFlatDomain {
        private String name;
        private BigDecimal first;
        private BigDecimal second;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public BigDecimal getFirst() {
            return first;
        }

        public void setFirst(BigDecimal first) {
            this.first = first;
        }

        public BigDecimal getSecond() {
            return second;
        }

        public void setSecond(BigDecimal second) {
            this.second = second;
        }
    }

}

Most helpful comment

Reproduced with jackson 2.5.1
Workaround possible with simple Deserializer:

public class MoneyDeserializer extends JsonDeserializer<BigDecimal> {

    private NumberDeserializers.BigDecimalDeserializer delegate = NumberDeserializers.BigDecimalDeserializer.instance;

    @Override
    public BigDecimal deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        BigDecimal bd = delegate.deserialize(jp, ctxt);
        bd = bd.setScale(2, RoundingMode.HALF_UP);
        return bd;
    }    
}

Then us the deserializer like this to make the test pass

private static class MyInnerDomain {
    @JsonDeserialize(using=MoneyDeserializer.class)
    private BigDecimal second;

All 9 comments

Odd. It'd seem scales should be the same. There isn't much support to change handling of BigDecimal, not sure why unwrapping would change anything.
Well, except... maybe it is due to buffering needed. If so, TokenBuffer would be the culprit.

Reproduced with jackson 2.5.1
Workaround possible with simple Deserializer:

public class MoneyDeserializer extends JsonDeserializer<BigDecimal> {

    private NumberDeserializers.BigDecimalDeserializer delegate = NumberDeserializers.BigDecimalDeserializer.instance;

    @Override
    public BigDecimal deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        BigDecimal bd = delegate.deserialize(jp, ctxt);
        bd = bd.setScale(2, RoundingMode.HALF_UP);
        return bd;
    }    
}

Then us the deserializer like this to make the test pass

private static class MyInnerDomain {
    @JsonDeserialize(using=MoneyDeserializer.class)
    private BigDecimal second;

I did the same kind of workarround using a JsonDeserializer but I used a module to make it global :

public class BigDecimalMoneyDeserializer extends JsonDeserializer<BigDecimal> {

    @Override
    public BigDecimal deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        return jp.getDecimalValue().setScale(2, BigDecimal.ROUND_HALF_UP);
    }
}
public class CustomObjectMapper extends ObjectMapper {

    public CustomObjectMapper() {
        super();

        SimpleModule testModule = new SimpleModule("my-module", new Version(1, 0, 0, null))
                .addDeserializer(BigDecimal.class, new BigDecimalMoneyDeserializer());

        this.registerModule(testModule);
    }

}

I'd stumbled on this when poking around before submitting #965, which seemed different, but due to @JsonUnwrapped also using TokenBuffer it sounds just like it, the scale changing as a side effect of coercion through double.

Patch 2.6.3 was just released, and it does contain one fix to BigDecimal handling w/ TokenBuffer.
May not be related to problem here, but worth mentioning.

Assuming this is fixed.

@azzoti that was exactly what I'm looking for, and it work for me. Thanks!

@cowtowncoder 鈥斅營 just ran across this in my own code. I've confirmed that the test case in the ticket fails in the latest version of Jackson: 2.11.1.

Here's the JUnit 5 test I wrote that verifies this same functionality:

import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class BigDecimalTrailingZeroesTest {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Test
    public void bigDecimalTrailingZeroes() throws JsonProcessingException {
        // Passes
        assertEquals(new BigDecimal("5.00"),
                objectMapper.readValue("{\"value\": 5.00}", BigDecimalHolder.class).getValue());
    }

    @Test
    public void unwrappedBigDecimalTrailingZeroes() throws JsonProcessingException {
        // org.opentest4j.AssertionFailedError:
        // Expected :5.00
        // Actual   :5.0
        assertEquals(new BigDecimal("5.00"),
                objectMapper.readValue("{\"value\": 5.00}", NestedBigDecimalHolder.class).getHolder().getValue());
    }

    private static class BigDecimalHolder {
        private BigDecimal value;

        public BigDecimal getValue() {
            return value;
        }

        public void setValue(BigDecimal value) {
            this.value = value;
        }
    }

    private static class NestedBigDecimalHolder {
        @JsonUnwrapped
        private BigDecimalHolder holder;

        public BigDecimalHolder getHolder() {
            return holder;
        }

        public void setHolder(BigDecimalHolder holder) {
            this.holder = holder;
        }
    }
}

@mjustin Please file a new issue (with possible ref to this as background); I do not usually re-open closed issues as that complicates release notes significantly.

Was this page helpful?
0 / 5 - 0 ratings