68

Currently, I'm using Jackson to send out JSON results from my Spring-based web application.

The problem I'm having is trying to get all money fields to output with 2 decimal places. I wasn't able to solve this problem using setScale(2), as numbers like 25.50 are truncated to 25.5 etc

Has anyone else dealt with this problem? I was thinking about making a Money class with a custom Jackson serializer... can you make a custom serializer for a field variable? You probably can... But even still, how could I get my customer serializer to add the number as a number with 2 decimal places?

Eric
  • 6,563
  • 5
  • 42
  • 66
ControlAltDel
  • 33,923
  • 10
  • 53
  • 80

7 Answers7

94

You can use a custom serializer at your money field. Here's an example with a MoneyBean. The field amount gets annotated with @JsonSerialize(using=...).

public class MoneyBean {
    //...

    @JsonProperty("amountOfMoney")
    @JsonSerialize(using = MoneySerializer.class)
    private BigDecimal amount;

    //getters/setters...
}

public class MoneySerializer extends JsonSerializer<BigDecimal> {
    @Override
    public void serialize(BigDecimal value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
            JsonProcessingException {
        // put your desired money style here
        jgen.writeString(value.setScale(2, BigDecimal.ROUND_HALF_UP).toString());
    }
}

That's it. A BigDecimal is now printed in the right way. I used a simple testcase to show it:

@Test
public void jsonSerializationTest() throws Exception {
     MoneyBean m = new MoneyBean();
     m.setAmount(new BigDecimal("20.3"));

     ObjectMapper mapper = new ObjectMapper();
     assertEquals("{\"amountOfMoney\":\"20.30\"}", mapper.writeValueAsString(m));
}
Steve
  • 18,660
  • 4
  • 34
  • 27
  • 11
    Nice approach, but it prints it as a string -- not a numeric type in the JSON output. – jro Jul 01 '13 at 19:55
  • 61
    From a business perspective, this is a terrible approach. Money should not be rounded at serialization time. If you want to *pad* with trailing zeros (different than blindly setting the scale), then you must do so without `ROUND_HALF_UP`. Also, different currencies expect different numbers of trailing decimals. – Peter Davis Jul 23 '14 at 15:36
  • 4
    jro, If writeNumber() is used instead of writeString() in Steve's serialize(), then the field will show as a number in the JSON. – Matthew Miling Sep 19 '17 at 22:35
  • @PeterDavis yes and no. It really depends on what you're serializing it for. In the real world Money has only two decimal places so if, for example, your API returns 12.4999990008212354, what should the actual real world value be 12.49 or 12.50? In a taxation world they prefer that you pay 12.50. – Maksym Bykovskyy May 03 '19 at 17:58
  • 13
    @MaksymBykovskyy The point isn't that rounding shouldn't happen, it's that these rounding rules are business logic and they shouldn't be being applied when serializing. They need to applied before you get to serialization, otherwise you're mixing concerns. – AndrewW Jun 14 '19 at 06:04
  • JsonProcessingException should not be in the exception list of the method... you can update your answer to remove this. – fergal_dd Dec 01 '21 at 14:58
  • Is there any way to adjust this problem to make sure it scale a bigDecimal with a given scale? eg: private BigDecimal bg1; private BigDecimal bg2; - here I want to serialize bg1 with scale 2 and bg2 with scale 4 . Can we adjust this solution ? (Obviously we can create different serializer and annotate these in our POJO, but here I am looking for a generalized solution). – avi_panday Dec 29 '22 at 00:02
  • @jro, I added an example with serialiser which we can customise and provide format and shape. It allows to define format and shape per field: [Java to Jackson Serializer with custom scale](https://stackoverflow.com/a/74961350/51591) – Michał Ziober Dec 30 '22 at 12:36
51

You can use @JsonFormat annotation with shape as STRING on your BigDecimal variables. Refer below:

 import com.fasterxml.jackson.annotation.JsonFormat;

  class YourObjectClass {

      @JsonFormat(shape=JsonFormat.Shape.STRING)
      private BigDecimal yourVariable;

 }
Sahil Chhabra
  • 10,621
  • 4
  • 63
  • 62
  • 3
    Yes, you are right. This option is available since Jackson 2.9.5: https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.9.5 – mladzo Oct 17 '18 at 18:34
  • 1
    It worked for me. Love this answer. This is pretty easy and clean way of doing things. – JavaGeek Nov 27 '18 at 15:58
  • Surprisingly it doesn't work for me (Spring Boot 2.3.7) :( While `@JsonSerialize` from the accepted answer does. – FlasH from Ru Jan 03 '21 at 20:42
  • I liked this solution. My BigDecimal value was being serialized as: "amount": {"value": "2836.00n", "type": "Big Number"} instead of 2836.00``. My problem now is that now is being output as String and not as a numeric value. "2836.00" – Ivan Jul 02 '23 at 00:51
  • 1
    @Ivan Thats because you must be using `JsonFormat.Shape.STRING` as mentioned in the answer. If you wish to have a numeric value, use `JsonFormat.Shape.NUMBER` (or any other equivalent shape) instead. – Sahil Chhabra Jul 16 '23 at 08:26
40

Instead of setting the @JsonSerialize on each member or getter you can configure a module that use a custome serializer for a certain type:

SimpleModule module = new SimpleModule();
module.addSerializer(BigInteger.class, new ToStringSerializer());
objectMapper.registerModule(module);

In the above example, I used the to string serializer to serialize BigIntegers (since javascript can not handle such numeric values).

Modi
  • 2,200
  • 4
  • 23
  • 37
  • 1
    *where* do I define the customer serializer? This is exactly what I want to do, but i can;t work out where to put this code. – DaveH May 20 '15 at 15:46
  • Where ever you like, you just have to implement com.fasterxml.jackson.databind.JsonSerializer – Modi May 20 '15 at 16:35
  • Can you be a bit more specific? I still don't get where to put it. – Wouter Jan 15 '16 at 15:51
  • 1
    I tried this, but it only works on the original class you are trying to serialize. If the object has properties of this type, they are not serialized using this serializer (I'm using Spring boot 1.3) – Wouter Mar 06 '16 at 21:32
  • Spring's boot is not what matters here, it's Jackson's capability. – Modi Mar 07 '16 at 09:39
  • @DaveH as part of the objectMapper initialization ObjectMapper objectMapper = new ObjectMapper() .registerModule(module); – Novaterata Mar 22 '17 at 13:33
  • 1
    @DaveH: If your `SpringMvcConfiguration` extends `DelegatingWebMvcConfiguration`, then you can override `extendMessageConverters()` method, find `MappingJackson2HttpMessageConverter` in the list of converters and then register SimpleModule like that: `mappingJackson2HttpMessageConverter.getObjectMapper().registerModule(simpleModule)`. That way you won't override existing `mappingJackson2HttpMessageConverter`. – Kacper86 Aug 23 '17 at 09:14
17

I'm one of the maintainers of jackson-datatype-money, so take this answer with a grain of salt since I'm certainly biased. The module should cover your needs and it's pretty light-weight (no additional runtime dependencies). In addition it's mentioned in the Jackson docs, Spring docs and there were even some discussions already about how to integrate it into the official ecosystem of Jackson.

whiskeysierra
  • 5,030
  • 1
  • 29
  • 40
6

As Sahil Chhabra suggested you can use @JsonFormat with proper shape on your variable. In case you would like to apply it on every BigDecimal field you have in your Dto's you can override default format for given class.

@Configuration
public class JacksonObjectMapperConfiguration {

    @Autowired
    public void customize(ObjectMapper objectMapper) {
         objectMapper
            .configOverride(BigDecimal.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING));
    }
}
dswiecki
  • 138
  • 2
  • 11
  • This feature is working only from Jackson version `2.9.5` (https://github.com/FasterXML/jackson-databind/issues/1911) I've encountered an issue where this was the resolution in a library used by our code, but our application is fixed to an older version of jackson. – Dániel Szabó Jul 01 '21 at 14:49
5

I had the same issue and i had it formatted into JSON as a String instead. Might be a bit of a hack but it's easy to implement.

private BigDecimal myValue = new BigDecimal("25.50");
...
public String getMyValue() {
    return myValue.setScale(2, BigDecimal.ROUND_HALF_UP).toString();
}
user1145065
  • 268
  • 4
  • 11
1

Inspired by Steve, and as the updates for Java 11. Here's how we did the BigDecimal reformatting to avoid scientific notation.

public class PriceSerializer extends JsonSerializer<BigDecimal> {
    @Override
    public void serialize(BigDecimal value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
        // Using writNumber and removing toString make sure the output is number but not String.
        jgen.writeNumber(value.setScale(2, RoundingMode.HALF_UP));
    }
}
Eric Tan
  • 1,377
  • 15
  • 14