CashAccount Journal, Spring Boot Serial and Deserialization of Java Money, Day 6

Donnie Z
5 min readJun 8, 2024

--

On Day 5 instead of using “to” and “from” and positive or negative balance on accounts and transactions, we used Debit Credit. Another changes that we did is replacing BigDecimal type with Java Money MonetaryAmount for balance and currency. This article will show how it is done plus a bit highlight on postman runner for testing.

Deserialization Exception

Changing balance type on entity into MonetaryAmount changed the database field as well. On local development, the database tables is recreated every time springboot started because we use this on property spring.jpa.hibernate.ddl-auto=create-drop. Because of that, the settings tables are empty again and to insert the AccountType settings develop hit POST /accountTypes end point, after changed the request body to (maybe) accommodate MonetaryAmount like this:

# // may be this works

$ curl --location --request POST 'http://127.0.0.1:12080/accountTypes' \
--header 'Content-Type: application/json' \
--data-raw '{
"typeCode": "DRAWERS",
"name": "Physical Drawer",
"balanceSheetEntry": "ASSETS",
"minimumBalance": "USD 0",
"notes": "Physical Drawer at Bank Branch"
}'


# // or may be this works

$ curl --location --request POST 'http://127.0.0.1:12080/accountTypes' \
--header 'Content-Type: application/json' \
--data-raw '{
"typeCode": "DRAWERS",
"name": "Physical Drawer",
"balanceSheetEntry": "ASSETS",
"minimumBalance": {
"amount": "0",
"currency": "USD"
},
"notes": "Physical Drawer at Bank Branch"
}'

However both failed with this error log:

2024-06-08T13:55:58.999+08:00 ERROR 34448 --- [med-spring-boot-demo] [io-12080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class javax.money.MonetaryAmount]] with root cause

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `javax.money.MonetaryAmount` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 5, column: 23] (through reference chain: com.github.donniexyz.demo.med.entity.AccountType["minimumBalance"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67) ~[jackson-databind-2.15.4.jar:2.15.4]
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1915) ~[jackson-databind-2.15.4.jar:2.15.4]
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:414) ~[jackson-databind-2.15.4.jar:2.15.4]

We can see the error is coming from ObjectMapper during deserialization. So now we just need to find proper ObjectMapper module, import it, configure it, and everything should work.

Enter jackson-datatype-money

After quick search on internet and github, I found this jackson-datatype-money library which seems to be what we need. Without further ado, the README mention we need to:

1. Add this into pom.xml:

<dependency>
<groupId>org.zalando</groupId>
<artifactId>jackson-datatype-money</artifactId>
<version>${jackson-datatype-money.version}</version>
</dependency>

2. Register MoneyModule into ObjectMapper

ObjectMapper mapper = new ObjectMapper()
.registerModule(new MoneyModule());

3. This is the serialized form, and I assume the deserializer will take up that form as well.

{
"amount": 99.95,
"currency": "EUR"
}

Ok, next we will create the spring configuration for that.

Spring Configuration

A new configuration class com.github.donniexyz.demo.med.config.MedDemoApplicationConfiguration created

@Primary
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
return registerOurModules(objectMapper);
}

@NotNull
private static ObjectMapper registerOurModules(ObjectMapper objectMapper) {
objectMapper.registerModule(new Hibernate6Module());
objectMapper.registerModule(new MoneyModule());
return objectMapper;
}

Test and Result #1

Test with MonetaryAmount value in String form

Test and Result #2 — Success

Test with MonetaryAmount value in Object form

We can see that the MonetaryAmount is a separate object with two fields. No wonder not many project excited to use that type directly. We could write our own serializer and deserializer so it can be represented in “USD 10” format as default.

Custom Serializer & Deserializer

Serializer

This custom serializer will returns “minimumBalance”: “USD 10” instead of object.

public class MonetaryAmountSerializer extends StdSerializer<MonetaryAmount> {

public MonetaryAmountSerializer() {
super(MonetaryAmount.class);
}
public MonetaryAmountSerializer(Class<?> vc) {
super(vc, false);
}

@Override
public void serialize(MonetaryAmount value, JsonGenerator json, SerializerProvider provider) throws IOException {
json.writeString(value.toString());
}

}

Deserializer

This deserializer able to process both “minimumBalance”: “USD 10” or “minimumBalance”: { “amount”: 10, “currency”: “USD”} into MonetaryAmount.

public class MonetaryAmountDeserializer extends StdDeserializer<MonetaryAmount> {

public MonetaryAmountDeserializer() {
this((Class<?>) null);
}
public MonetaryAmountDeserializer(Class<?> vc) {
super(vc);
}

@Override
public MonetaryAmount deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
JsonNode node = p.getCodec().readTree(p);
if (node.isTextual())
return Money.parse(node.asText());
if (node.isObject()) {
JsonNode amount = node.get("amount");
JsonNode currency = node.get("currency");
// not sure whether to use default value 0 USD when those fields are null..
return Money.of(new BigDecimal(amount.asText("0")),
CashAccountConstants.MA.CURRENCY_UNIT_MAP.get(currency.asText(CashAccountConstants.MA.ALLOWED_CURRENCY_NAMES.get(0))));
}
return null;
}
}

Configuration

On spring config we define ObjectMapper bean and register MoneyModule from jackson-datatype-money and also register our custom MonetaryAmount ser & deser which overrides the ser & deser from MoneyModule. We could skip registering MoneyModule but it contains several other ser & deser that I want to keep for now.

I would also like to be able to toggle the default MonetaryAmount deserializer whether to return string form or object form using application property. Our custom deserializer works for both form, so we need to make sure that objectMapper use our deserializer instead of MoneyModule’s.

Our configuration:

@Configuration
public class MedDemoApplicationConfiguration {

//@Primary
@Bean
public ObjectMapper objectMapper(@Value("${donniexyz.med.MonetaryAmount.serializeAsString.enabled:false}") boolean serMonetaryAmountAsString) {
ObjectMapper objectMapper = new ObjectMapper();
return registerOurModules(objectMapper, serMonetaryAmountAsString);
}

@NotNull
public static ObjectMapper registerOurModules(ObjectMapper objectMapper, boolean serMonetaryAmountAsString) {
objectMapper.registerModule(new Hibernate6Module());
objectMapper.registerModule(new MoneyModule());
SimpleModule module = new SimpleModule("MonetaryAmountAsStringModule");
module.addDeserializer(MonetaryAmount.class, new MonetaryAmountDeserializer());
if (serMonetaryAmountAsString) module.addSerializer(MonetaryAmount.class, new MonetaryAmountSerializer());
objectMapper.registerModule(module);
return objectMapper;
}
}

Our custom ser & deser module is registered after MoneyModule. This makes our custom ser & deser has higher priority to be picked up by objectMapper. I am not sure whether this is consistent across different versions. That means developer need to put this into unit test to make sure the behavior does not breaks.

Test

With spring.jpa.hibernate.ddl-auto=create-drop the database tables are dropped and recreated every time springboot started. To easily insert those settings developer created postman to be launched by runner. The runner executes Requests sequentially. The postman file is committed into git, for other to reuse.

Postman file. runner/settings collection to be executed by postman runner

Conclusion

Today Java Money serialization and deserialization has been resolved. There is postman collection that can be use to easily populates settings table. And as bonus, postman runner also can be used to do end to end regression testing.

Postman Collection Runner, without testing validation.

References

  1. Source code can be found on donniexyz/med-spring-boot-demo tag article/med-6b. EDIT: tag article/med-6b contains fixes for Controllers and more complete Postman Collection for Runner!
  2. Java Money And Currency, from Baeldung
  3. jackson-datatype-money by Zalado on Github

--

--