lastminute.com logo

Technology

A custom module for Jackson object mapper using Java Service Provider

fabrizio_duroni
fabrizio duroni
alex_stabile
alex stabile

Sometimes you have custom Jackson object mapper imported from external modules/libraries. How can you customize their serialization/deserialization? Let's go to discover the power of Java Service Provider Interface.


Alex is a senior software engineer with 9 years of experience. He has a strong knowledge of backend (using a lot of different languages) and frontend (web and mobile) development.

In the last weeks I started to work in a new team on a new project at lm group. One of the goals we have is to renew the foundations of the company software overall architecture by introducing in the development workflow new technologies. In particular, we are using Axon, a framework to help developer to create Domain Driven Design applications that leverage specific architectural pattern like CQRS and Event Sourcing.
During the definition of a new microservice we had to customize the object mapper used by Axon, defined in one maven module (that will probably be integrated in our app-framework framework if we decide to stick with it) from one of our new app specific module without creating any kind of coupling/dependencies. This is how me and Alex Stabile discovered the power of Java Service Provider interface, used by Jackson Object Mapper to register external custom Modules in order to apply application specific serialization/deserialization procedures. So everything is setup and ready, let’s go!! emoji-rocket

Implementation

Let’s start by defining a simple webapp application defined in one maven module. This app exposes a couple of endpoints in the ProductRestController, a standard spring boot RestController. These endpoints let the client add and retrieve Product information by using the ProductRepository. Below you can find the controller code.

@RestController
class ProductRestController(
    private val productRepository: ProductRepository
) {

    @GetMapping("/product/{idProduct}")
    fun getProductFor(@PathVariable idProduct: Long): ResponseEntity<*> =
        productRepository
            .get(idProduct)
            .fold(
                { ResponseEntity.notFound().build() },
                { ResponseEntity.ok(it) }
            )

    @PostMapping("/product/add")
    fun add(@RequestBody product: Product): ResponseEntity<*> =
        productRepository
            .add(product)
            .fold(
                { ResponseEntity.internalServerError().build() },
                { ResponseEntity.ok(it) }
            )
}

The Product and ProductRepository classes are really simple. ProductRepository is an “in memory repository” that exposes a couple of methods to get and add products. This repository has been created using Arrow (see the result type of type Option).

class ProductRepository {
    private val products = mutableListOf(
        Product(
            1,
            "A product",
            Money.of(BigDecimal("100.00"), "EUR")
        ),
        Product(
            2,
            "Another product",
            Money.of(BigDecimal("150.00"), "EUR")
        ),
        Product(
            3,
            "Yet another product",
            Money.of(BigDecimal("120.50"), "EUR")
        )
    )

    fun get(idProduct: Long): Option<Product> =
        products.find { it.idProduct == idProduct }.toOption()

    fun add(product: Product): Option<Unit> =
        products
            .find { it.idProduct == product.idProduct }
            .toOption()
            .fold(
                {
                    products.add(product)
                    Unit.some()
                },
                { None }
            )
}

Product contains the information about our products. Here comes the interesting part: the amount property has been defined using the Money type from the JavaMoney library.

import org.javamoney.moneta.Money

data class Product(
    val idProduct: Long,
    val description: String,
    val amount: Money
)

The fact that we are using the Money type in the amount means that in the response and request of te endpoints we showed above, the object to be passed as JSON should be with all the fields of this type. For example the /product/{idProduct} endpoint will return us the following response.

{
  "idProduct": 3,
  "description": "Yet another product",
  "amount": {
    "currency": {
      "context": {
        "providerName": "java.util.Currency",
        "empty": false
      },
      "currencyCode": "EUR",
      "numericCode": 978,
      "defaultFractionDigits": 2
    },
    "number": 120.5,
    "context": {
      "precision": 256,
      "fixedScale": false,
      "maxScale": -1,
      "amountType": "org.javamoney.moneta.Money",
      "providerName": null,
      "empty": false
    },
    "numberStripped": 120.5,
    "zero": false,
    "positive": true,
    "positiveOrZero": true,
    "negative": false,
    "negativeOrZero": false,
    "factory": {
      "defaultMonetaryContext": {
        "precision": 0,
        "fixedScale": false,
        "maxScale": 63,
        "amountType": "org.javamoney.moneta.Money",
        "providerName": null,
        "empty": false
      },
      "maxNumber": null,
      "minNumber": null,
      "amountType": "org.javamoney.moneta.Money",
      "maximalMonetaryContext": {
        "precision": 0,
        "fixedScale": false,
        "maxScale": -1,
        "amountType": "org.javamoney.moneta.Money",
        "providerName": null,
        "empty": false
      }
    }
  }
}

This is not what we want!!! emoji-fearful What we would like to have as response is something like the following json.

{
  "idProduct": 3,
  "description": "Yet another product",
  "amount": {
    "amount": "120.5",
    "currency": "EUR"
  }
}

We also have the same problem in the /product/add endpoint, where we are forced to send a request with the payload above, the one with all the Money fields, in order to add a product. How can we customize the way the Jackson ObjectMapper serialize/deserialize Money instances? We can write a custom Module for it. By defining a custom module we can add ad-hoc serializers and deserializers. We will define our object mapper module in a new maven module called money-module.
Let’s start by defining theMoneyDeserializer. It will give us the ability to define a Money instance from the data contained in a JSON that we are deserializing.

open class MoneyDeserializer : StdDeserializer<Money>(Money::class.java) {
    override fun deserialize(jsonParser: JsonParser, obj: DeserializationContext): Money {
        val node: JsonNode = jsonParser.codec.readTree(jsonParser)
        val amount = BigDecimal(node.get("value").asText())
        val currency: String = node.get("currency").asText()

        return Money.of(amount, currency)
    }
}

The MoneySerializer let us defined which fields of a Money instance we want to write to a json. We can also the define the specific type we want to use in the json for each one of them.

open class MoneySerializer : StdSerializer<Money>(Money::class.java) {
    @Throws(IOException::class)
    override fun serialize(money: Money, jsonGenerator: JsonGenerator, serializerProvider: SerializerProvider) {
        jsonGenerator.writeStartObject()
        jsonGenerator.writeStringField("amount", money.numberStripped.toPlainString())
        jsonGenerator.writeStringField("currency", money.currency.toString())
        jsonGenerator.writeEndObject()
    }
}

Now we can add our custom serializer/deserializer to our MoneyModule definition.

class MoneyModule: SimpleModule() {
    override fun getModuleName(): String = this.javaClass.simpleName

    override fun setupModule(context: SetupContext) {
        val serializers = SimpleSerializers()
        serializers.addSerializer(Money::class.java, MoneySerializer())
        context.addSerializers(serializers)

        val deserializers = SimpleDeserializers()
        deserializers.addDeserializer(Money::class.java, MoneyDeserializer())
        context.addDeserializers(deserializers)
    }
}

We are now at the core of our development: how do we load our custom module into our object mapper? Well we have different options based on our use case. If you’re using the DEFAULT Spring Boot object mapper you can just define a new @Bean for the MoneyModule that will be used by Spring Boot to load it(with its custom internal flow).
But this is not our case: in our ProductConfiguration we have defined a custom objectMapper bean.

@Configuration
class ProductConfiguration {
    @Bean
    fun productRepository(): ProductRepository = ProductRepository()

    @Bean
    @Primary
    fun objectMapper(): ObjectMapper =
        ObjectMapper()
            .findAndRegisterModules()
            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
}

If you look closely to our ObjectMapper definition you can see something interesting: before returning the instance creation there is a call to the findAndRegisterModules. What does this method do? It contains the core feature of Jackson Modules load emoji-heart_eyes. This method is in charge of loading external third party modules using the Java Service Provider interfaces.
This is a feature of Java 6 that let (library) developer write code to load and discovery third party plugins for their library implementations that matches a specific interface. In our case Jackson uses it to load every third party implementation that matches the Module interface. How does it work? The ServiceProvider will scan the classpath searching for service definition adhering to the base interface defined for the external/third party implementation.
This is done by searching for a specific file in the folders META-INF/services of the (maven) modules in the classpath, named with the fully qualified name of the interface loaded by the ServiceProvider and that contains the fully qualified name of our implementation. So in our case, to load our MoneyModule contained in the maven module money-module, we just have to add a file named com.fasterxml.jackson.databind.Module in the META-INF/services folder of the money-module and inside it write the fully qualified name of our MoneyModule implementation.

custom jackson module
custom jackson module findAndRegisterModules

That’s it!!! emoji-rocket With the implementation above we have a custom Jackson Module that will be loaded by its ObjectMapper automatically without creating any dependencies. In this way you will be able to publish your custom serializer/deserializer as custom maven artifacts and use them in all your projects (without copy/paste them) emoji-heart.

Conclusion

You can find the complete source code of the example show above in this Github repository. Stay tuned for new article on one of the technologies/Pattern above (Axon, CQRS, Event Sourcing, we have a lot of stuff to talk about emoji-heart ).


Read next

SwiftUI and the Text concatenations super powers

SwiftUI and the Text concatenations super powers

fabrizio_duroni
fabrizio duroni
marco_de_lucchi
marco de lucchi

Do you need a way to compose beautiful text with images and custom font like you are used with Attributed String. The Text component has everything we need to create some sort of 'attributed text' directly in SwiftUI. Let's go!!! [...]

A Monorepo Experiment: reuniting a JVM-based codebase

A Monorepo Experiment: reuniting a JVM-based codebase

luigi_noto
luigi noto

Continuing the Monorepo exploration series, we’ll see in action a real-life example of a monorepo for JVM-based languages, implemented with Maven, that runs in continuous integration. The experiment of reuniting a codebase of ~700K lines of code from many projects and shared libraries, into a single repository. [...]