All Articles

Custom Jackson serialization per class, with additional logic

JSON
Sometimes, you need a little bit of customization in your communication

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. ;)