In previous articles, we’ve created some endpoints that cover monetary values, i.e. cash and cryptocurrency accounts.
Next, we’ve added the option for multiple currencies. In this article, we’ll cover how to create a custom response style for those values.
In order to do so, we’ll create a Serializer which will be added to the ObjectMapper, so that it will be used whenever that class is encountered in a response.
Problem description
There are many different reasons why you might want to create a custom serializer that doesn’t reflect the Data Structure. We’re not going into all the possible reasons here. However, in this case, we will have many places that will cover the monetary value, customized into the user’s preferred currency. In our tables though, the currency will always be either the defined currency, for cash accounts for instance, or USD in other cases, e.g. for cryptocurrency accounts.
So, in order to prevent a complicated logic for mapping, which would need to occur in lots of places, we will simply have one, separate data class that will always contain the monetary value. Also, we will need only one custom Serializer, which will then take into account the user’s preference.
This way, while it’s not immediately the most straightforward option for only one usecase, it will make the situation immensely easier to add additional cases.
The DTO to be serialized is the following, very simple one:
data class ValueDto(val value: Double)
Serializer
Now, to create a Serializer is quite easy. We simply need to extend the StdSerializer
, override the serialize
method, and define our logic in there.
class ValueDtoSerializer(
t: Class<ValueDto>,
private val userService: UserService,
private val currencyService: CurrencyService
) : StdSerializer<ValueDto>(t) {
override fun serialize(dto: ValueDto, jgen: JsonGenerator, sp: SerializerProvider) {
val preferredCurrency = userService.getLoggedInUser().preferences.currency
val currencyRate = currencyService.getConversionRate(preferredCurrency)
jgen.writeStartObject();
jgen.writeNumberField("value", dto.value * currencyRate);
jgen.writeStringField("currency", preferredCurrency.currencyCode);
jgen.writeEndObject();
}
}
As you can see, there’s quite some components injected in there. This is not usually the case, but it doesn’t really change
much. The important part is in the jgen.writeXXXField
lines. Here, we can define the type, the name and the value of the
part that we want to serialize.
In our example, we state that the ValueDto
class should be serialized from it’s only member, value: Double
, into an
object containing value
and currency
. The value for value
will not be the same however as handled previously, but instead
converted into the preferred currency.
ObjectMapper
After the Serializer is defined, there are two ways to make the application aware that this Serializer is to be used to serialize the object:
- On the class
- In the ObjectMapper
On the class
To do so, we can use the @JsonSerialize
annotation. The data class would then look as follows:
@JsonSerialize(using = ValueDtoSerializer::class)
data class ValueDto(val value: Double)
Unfortunately, this will not work for our Serializer, as we need additional items injected into it. This, we cannot do through the annotation.
In the ObjectMapper
Even without the reason mentioned above, I prefer to handle these matters in the ObjectMapper anyway. The reason for this is that I like to have such definitions in one, central place. If they’re used as some annotation, it could take longer during debugging.
That is, naturally, a personal preference.
In order to have this in the ObjectMapper, we need to configure it as follows:
@Bean
@Primary
fun objectMapper(currencyService: CurrencyService, userService: UserService): ObjectMapper {
val objectMapper = ObjectMapper().registerKotlinModule()
val valueDtoSerializer = ValueDtoSerializer(ValueDto::class.java, userService, currencyService)
val valueDtoModule = SimpleModule()
valueDtoModule.addSerializer(ValueDto::class.java, valueDtoSerializer)
objectMapper.registerModule(valueDtoModule)
return objectMapper
}
Result
Okay, now to test this, we create a small Unit Test:
@SpringBootTest
internal class RestConfigurationTest {
@MockkBean
private lateinit var currencyService: CurrencyService
@MockkBean
private lateinit var userService: UserService
@Autowired
private lateinit var objectMapper: ObjectMapper
@Test
fun objectMapper_valueDto_correctlySerialized() {
every { userService.getLoggedInUser() } returns UserFixture.user()
every { currencyService.getConversionRate(any()) } returns 2.0
val valueDto = ValueDto(1.0)
val writeValueAsString = objectMapper.writeValueAsString(valueDto)
assertThat(writeValueAsString).isEqualTo("{\"value\":2.0,\"currency\":\"CHF\"}")
}
}
Of course, it should also be added as an assertion in the sequence of the integration tests. That’s out of the scope of this article, though.
So, now you know how to do custom conversions. Do not go overboard with it though, and don’t make items too fancy. ;)