6

I have POJO like where I have a BigDecimal field:

public class Foo implements Serializable {

   private BigDecimal amount;
}

I want to have the BigDecimal value up to 2 decimal place only. Is there any annotation exist using which I can directly change it's value at the field level itself? I cannot change it's type as well.

Though it can be done with getter and ObjectMapper within the application.

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
ernitingoel
  • 621
  • 2
  • 9
  • 24

2 Answers2

10

When you want to set scale you need to take care about rounding. You have some options such as ROUND_HALF_EVEN and you need to decide which rounding mode to use.

To intercept BigDecimal deserialisation you can write custom deserialiser. Below example shows how to do that, we can extend default one and set scale after deserialzation:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        ObjectMapper mapper = new ObjectMapper();

        Foo person = mapper.readValue(jsonFile, Foo.class);
        System.out.println(person);
    }
}

class BigDecimal2JsonDeserializer extends NumberDeserializers.BigDecimalDeserializer {

    @Override
    public BigDecimal deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        BigDecimal value = super.deserialize(p, ctxt);

        // set scale
        value = value.setScale(2, BigDecimal.ROUND_HALF_EVEN);

        return value;
    }
}

class Foo {

    @JsonDeserialize(using = BigDecimal2JsonDeserializer.class)
    private BigDecimal amount;

    public BigDecimal getAmount() {
        return amount;
    }

    public void setAmount(BigDecimal amount) {
        this.amount = amount;
    }

    @Override
    public String toString() {
        return "Foo{" +
                "amount=" + amount +
                '}';
    }
}

For below JSON payload:

{
  "amount": 16.127
}

above app prints:

Foo{amount=16.13}
Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
0

For you Kotlin lovers, I implemented this BigDecimal ContextualSerializer where the number of decimal places can be configured using an annotation. Although it is strange place to introduce rounding in the serializer, for my case this was not an issue. Check for yourselves if rounding can be an issue.

The Serializer class:

class CustomBigDecimalSerializer(private val decimalPlaces: Int = 2) : StdSerializer<BigDecimal>(BigDecimal::class.java),
        ContextualSerializer {

    override fun serialize(value: BigDecimal?, jsonGenerator: JsonGenerator?, serializerProvider: SerializerProvider?) {
        if (value != null) {
            jsonGenerator?.writeNumber(value.setScale(decimalPlaces, RoundingMode.HALF_UP))
        } else {
            jsonGenerator?.writeNull()
        }
    }

    override fun createContextual(provider: SerializerProvider?, property: BeanProperty?): JsonSerializer<*> {
        val decimalPlaces = property?.getAnnotation(DecimalPlaces::class.java)
        return CustomBigDecimalSerializer(decimalPlaces?.numberOfDigits ?: 2)
    }
}

The annotation that can be used to configure the number of decimal places:

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class DecimalPlaces(val numberOfDigits: Int)

And finally the usage of it in this 'unit test':

class CustomBigDecimalSerializerTest {

    @Test
    fun serializeBigDecimals(){
        val objectMapper: ObjectMapper = ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT)
        val container = BigDecimalContainer(BigDecimal(Math.PI))
        // no assertions, just print the serialized container
        println(objectMapper.writeValueAsString(container))
    }

    data class BigDecimalContainer(val rawValue: BigDecimal){

        @JsonSerialize(using = CustomBigDecimalSerializer::class)
        val defaultDecimalPlaces = rawValue

        @JsonSerialize(using = CustomBigDecimalSerializer::class)
        @DecimalPlaces(numberOfDigits = 1)
        val oneDecimalPlaces = rawValue

        @JsonSerialize(using = CustomBigDecimalSerializer::class)
        @DecimalPlaces(numberOfDigits = 2)
        val twoDecimalPlaces = rawValue

        @JsonSerialize(using = CustomBigDecimalSerializer::class)
        @DecimalPlaces(numberOfDigits = 3)
        val threeDecimalPlaces = rawValue

        @JsonSerialize(using = CustomBigDecimalSerializer::class)
        @DecimalPlaces(numberOfDigits = 10)
        val tenDecimalPlaces = rawValue
    }
}

The output of the test is:

{
  "rawValue" : 3.141592653589793115997963468544185161590576171875,
  "defaultDecimalPlaces" : 3.14,
  "oneDecimalPlaces" : 3.1,
  "twoDecimalPlaces" : 3.14,
  "threeDecimalPlaces" : 3.142,
  "tenDecimalPlaces" : 3.1415926536
}

Have fun with it!

gizit
  • 160
  • 1
  • 6