49

I'm using Jackson, with Spring MVC, to write out some simple objects as JSON. One of the objects, has an amount property, of type Double. (I know that Double should not be used as a monetary amount. However, this is not my code.)

In the JSON output, I'd like to restrict the amount to 2 decimal places. Currently it is shown as:

"amount":459.99999999999994

I've tried using Spring 3's @NumberFormat annotation, but haven't had success in that direction. Looks like others had issues too: MappingJacksonHttpMessageConverter's ObjectMapper does not use ConversionService when binding JSON to JavaBean propertiesenter link description here.

Also, I tried using the @JsonSerialize annotation, with a custom serializer.
In the model:

@JsonSerialize(using = CustomDoubleSerializer.class)
public Double getAmount()

And serializer implementation:

public class CustomDoubleSerializer extends JsonSerializer<Double> {
    @Override
    public void serialize(Double value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
        if (null == value) {
            //write the word 'null' if there's no value available
            jgen.writeNull();
        } else {
            final String pattern = ".##";
            //final String pattern = "###,###,##0.00";
            final DecimalFormat myFormatter = new DecimalFormat(pattern);
            final String output = myFormatter.format(value);
            jgen.writeNumber(output);
        }
    }
}

The CustomDoubleSerializer "appears" to work. However, can anyone suggest any other simpler (or more standard) way of doing this.

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
mlo55
  • 6,663
  • 6
  • 33
  • 26
  • 1
    One way could be to format the amount in the setter method and then set the value to the field. Thus `getAmount()` will return 2 decimal value. Not sure if it may cater to your requirement. This implementation may have side-effects if someone else is expecting on the precision of the field. – devang Jul 18 '12 at 00:33
  • 1
    A rounding serializer seems the right approach to me. Alternately, create two getters, i.e. `getAmountPrecise()` and `getAmountRounded()`, and only serialize the latter. – millimoose Jul 18 '12 at 11:49
  • Could you add, for example, any new method to POJO with this property? Could you change this class or it belongs to external library? – Michał Ziober Mar 19 '14 at 22:04
  • Maybe you can improve performance by using `private static final DecimalFormat formatter = new DecimalFormat(".##");` and the referencing the field `formatter` afterwards, since it's the same all the time anyways? – randers Aug 20 '15 at 19:54
  • Have you tried `@JsonFormat(shape = JsonFormat.Shape.NUMBER_FLOAT, pattern=...)`? But I think your way is standard. – WesternGun Apr 08 '19 at 14:20
  • @WesternGun Hi. I've tried approach you suggested, but couldn't get it working. Can you provide an example for this case? – Sarvar Nishonboyev Jun 20 '19 at 08:30

5 Answers5

10

I know that Double should not be used as a monetary amount. However, this is not my code.

Indeed, it should not. BigDecimal is a much better choice for storing monetary amounts because it is lossless and provides more control of the decimal places.

So for people who do have control over the code, it can be used like this:

double amount = 111.222;
setAmount(new BigDecimal(amount).setScale(2, BigDecimal.ROUND_HALF_UP));

That will serialize as 111.22. No custom serializers needed.

rustyx
  • 80,671
  • 25
  • 200
  • 267
7

I had a similar situation in my project. I had added the formatting code to the setter method of the POJO. DecimalFormatter, Math and other classes ended up rounding off the value, however, my requirement was not to round off the value but only to limit display to 2 decimal places.

I recreated this scenario. Product is a POJO which has a member Double amount. JavaToJSON is a class that will create an instance of Product and convert it to JSON. The setter setAmount in Product will take care of formatting to 2 decimal places.

Here is the complete code.

Product.java

package com;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Product {

    private String name;
    private Double amount;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Double getAmount() {
        return amount;
    }
    public void setAmount(Double amount) {
        BigDecimal bd = new BigDecimal(amount).setScale(2, RoundingMode.FLOOR);
        this.amount = bd.doubleValue();
    }

    @Override
    public String toString() {
        return "Product [name=" + name + ", amount=" + amount + "]";
    }
}

JavaToJSON.java

package com;

import java.io.File;
import java.io.IOException;

import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;

public class JavaToJSON {

    public static void main(String[] args){

        ObjectMapper mapper = new ObjectMapper();

        try {
            Product product = new Product();
            product.setName("TestProduct");
            product.setAmount(Double.valueOf("459.99999999999994"));

            // Convert product to JSON and write to file
            mapper.writeValue(new File("d:\\user.json"), product);

            // display to console
            System.out.println(product);

        } catch (JsonGenerationException e) {

            e.printStackTrace();

        } catch (JsonMappingException e) {

            e.printStackTrace();

        } catch (IOException e) {

            e.printStackTrace();

        }
    }

}

I haven't accumulated enough points so I am not able to upload the screenshots to show you the output.

Hope this helps.

  • 3
    You don't need a screenshot. If you want to show the results just do a few lines with spaces at the beginning (just like you do with code) – David Newcomb Sep 23 '15 at 14:44
3

Regarding what was stated above, I just wanted to fix a little something, so that people won't waste time on it as I did. One should actually use

BigDecimal.valueOf(amount).xxx

instead of

new BigDecimal(amount).xxx

and this is actually somehow critical. Because if you don't, your decimal amount will be messed up. This is a limitation of floating point representation, as stated here.

Olivier B.
  • 91
  • 2
2

Best way I have seen till now is to create a customized serializer and @JsonSerializer(using=NewClass.class). Wanted to try with @JsonFormat(pattern=".##") or so, but it may not work according one comment of OP(I think the formatter does not honor that)

See here: https://github.com/FasterXML/jackson-databind/issues/632

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;
    }    
}

BUT, although more convenient and less code is written, usually, deciding the scale of a field is concern of business logic, not part of (de)serialization. Be clear about that. Jackson should be able to just pass the data as-is.

WesternGun
  • 11,303
  • 6
  • 88
  • 157
-4

Note that 459.99999999999994 is effectively 460 and is expected to be serialized in this way. So, your logic should be trickier than just dropping digits. I might suggest something like:

Math.round(value*10)/10.0

You might want to put it into setter, and get rid of custom serialization.

theme
  • 361
  • 5
  • 7
  • 2
    changing the value like this can still result in a number with significant trailing digits when serialised. Rounding is _strictly_ a presentation issue. Try this in a Chrome console, for example - `(Math.round(Math.PI * 10) / 10.0).toFixed(20)`. The output is not `3.10000000000000000000` but `3.10000000000000008882`. – Alnitak Aug 12 '14 at 15:26
  • 1
    Answer is for java, not javascript. – theme Dec 18 '14 at 19:23
  • 2
    The treatment of IEEE-754 floating point math is identical in both languages. – Alnitak Dec 18 '14 at 21:38