|
| 1 | +--- |
| 2 | +title: "A custom module for Jackson object mapper using Java Service Provider" |
| 3 | +description: "Sometimes you have custom Jackson object mapper imported from external modules/libraries? How can you |
| 4 | +customize their serialization/deserialization? Let's go to discover the power of Java Service Provider Interface." |
| 5 | +date: 2022-03-18 |
| 6 | +image: ../images/posts/XXX.jpg |
| 7 | +tags: [java, kotlin, web development] |
| 8 | +comments: true |
| 9 | +math: false |
| 10 | +authors: [fabrizio_duroni, alex_stabile] |
| 11 | +--- |
| 12 | + |
| 13 | +*Sometimes you have custom Jackson object mapper imported from external modules/libraries? How can you |
| 14 | +customize their serialization/deserialization? Let's go to discover the power of Java Service Provider Interface.* |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +In the last weeks I started to work in a new team on a new project at [lm group](https://lmgroup.lastminute.com |
| 19 | +"lastminute"). One of the goals we have is to renew the foundations of the company software overall architecture by |
| 20 | +introducing in the development workflow new technologies. In particular, we are using [Axon](https://www.axoniq.io), |
| 21 | +a framework to help developer to create [Domain Driven Design](https://www.fabrizioduroni.it/2021/06/06/ddd-dictionary/) applications |
| 22 | +that leverage specific architectural pattern like [CQRS](https://martinfowler.com/bliki/CQRS.html) and |
| 23 | +[Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html). |
| 24 | +During the definition of a new microservice we had to customize the object mapper used by Axon, |
| 25 | +defined in one maven module (that will probably be integrated in our [app-framework framework](https://technology.lastminute.com/frontend-backend-languages-frameworks/) |
| 26 | +if we decide to stick with it) from one of our new app specific module *without creating any kind of |
| 27 | +coupling/dependencies*. This is how me and [Alex Stabile](https://www.linkedin.com/in/alex-stabile-a9316b94/) |
| 28 | +discovered the power of [Java Service Provider interface](https://www.baeldung.com/java-spi), used by [Jackson |
| 29 | +Object Mapper](https://www.baeldung.com/jackson-object-mapper-tutorial) to register external custom [Modules](https://fasterxml.github.io/jackson-databind/javadoc/2.7/com/fasterxml/jackson/databind/Module.html) in order |
| 30 | +to apply application specific serialization/deserialization procedures. |
| 31 | +Before starting with the implementation details, let me introduce Alex :rocket::clap:. He is a Senior Software |
| 32 | +Engineer with 9 years of experience. He is able to develop software application as a real Full stack developer, from |
| 33 | +the backend using a lot of different languages, to the frontend (web and mobile). |
| 34 | +So everything is setup and ready, let's go!! :rocket: |
| 35 | + |
| 36 | +#### Implementation |
| 37 | + |
| 38 | +Let's start by defining a simple `webapp` application defined in one maven module. This app exposes a couple of |
| 39 | +endpoints in the `ProductRestController`, a standard spring boot `RestController`. These endpoints let the client add |
| 40 | +and retrieve `Product` information by using the `ProductRepository`. Below you can find the controller code. |
| 41 | + |
| 42 | +```kotlin |
| 43 | +@RestController |
| 44 | +class ProductRestController( |
| 45 | + private val productRepository: ProductRepository |
| 46 | +) { |
| 47 | + |
| 48 | + @GetMapping("/product/{idProduct}") |
| 49 | + fun getProductFor(@PathVariable idProduct: Long): ResponseEntity<*> = |
| 50 | + productRepository |
| 51 | + .get(idProduct) |
| 52 | + .fold( |
| 53 | + { ResponseEntity.notFound().build() }, |
| 54 | + { ResponseEntity.ok(it) } |
| 55 | + ) |
| 56 | + |
| 57 | + @PostMapping("/product/add") |
| 58 | + fun add(@RequestBody product: Product): ResponseEntity<*> = |
| 59 | + productRepository |
| 60 | + .add(product) |
| 61 | + .fold( |
| 62 | + { ResponseEntity.internalServerError().build() }, |
| 63 | + { ResponseEntity.ok(it) } |
| 64 | + ) |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +The `Product` and `ProductRepository` classes are really simple. `ProductRepository` is an ["in memory |
| 69 | +repository"](https://martinfowler.com/bliki/InMemoryTestDatabase.html) that exposes a couple of methods to get and |
| 70 | +add products. This repository has been created using [Arrow](https://arrow-kt.io) (see the result type of type |
| 71 | +`Option`). |
| 72 | + |
| 73 | +```kotlin |
| 74 | +class ProductRepository { |
| 75 | + private val products = mutableListOf( |
| 76 | + Product( |
| 77 | + 1, |
| 78 | + "A product", |
| 79 | + Money.of(BigDecimal("100.00"), "EUR") |
| 80 | + ), |
| 81 | + Product( |
| 82 | + 2, |
| 83 | + "Another product", |
| 84 | + Money.of(BigDecimal("150.00"), "EUR") |
| 85 | + ), |
| 86 | + Product( |
| 87 | + 3, |
| 88 | + "Yet another product", |
| 89 | + Money.of(BigDecimal("120.50"), "EUR") |
| 90 | + ) |
| 91 | + ) |
| 92 | + |
| 93 | + fun get(idProduct: Long): Option<Product> = |
| 94 | + products.find { it.idProduct == idProduct }.toOption() |
| 95 | + |
| 96 | + fun add(product: Product): Option<Unit> = |
| 97 | + products |
| 98 | + .find { it.idProduct == product.idProduct } |
| 99 | + .toOption() |
| 100 | + .fold( |
| 101 | + { |
| 102 | + products.add(product) |
| 103 | + Unit.some() |
| 104 | + }, |
| 105 | + { None } |
| 106 | + ) |
| 107 | +} |
| 108 | +``` |
| 109 | + |
| 110 | +`Product` contains the information about our products. Here comes the interesting part: the `amount` property has |
| 111 | +been defined using the `Money` type from the [JavaMoney library](http://javamoney.github.io). |
| 112 | + |
| 113 | +```kotlin |
| 114 | +import org.javamoney.moneta.Money |
| 115 | + |
| 116 | +data class Product( |
| 117 | + val idProduct: Long, |
| 118 | + val description: String, |
| 119 | + val amount: Money |
| 120 | +) |
| 121 | +``` |
| 122 | + |
| 123 | +The fact that we are using the `Money` type in the amount means that in the response and request of te endpoints we |
| 124 | +showed above, the object to be passed as JSON should be with all the fields of this type. For example the |
| 125 | +`/product/{idProduct}` endpoint will return us the following response. |
| 126 | + |
| 127 | +```json |
| 128 | +{ |
| 129 | + "idProduct": 3, |
| 130 | + "description": "Yet another product", |
| 131 | + "amount": { |
| 132 | + "currency": { |
| 133 | + "context": { |
| 134 | + "providerName": "java.util.Currency", |
| 135 | + "empty": false |
| 136 | + }, |
| 137 | + "currencyCode": "EUR", |
| 138 | + "numericCode": 978, |
| 139 | + "defaultFractionDigits": 2 |
| 140 | + }, |
| 141 | + "number": 120.5, |
| 142 | + "context": { |
| 143 | + "precision": 256, |
| 144 | + "fixedScale": false, |
| 145 | + "maxScale": -1, |
| 146 | + "amountType": "org.javamoney.moneta.Money", |
| 147 | + "providerName": null, |
| 148 | + "empty": false |
| 149 | + }, |
| 150 | + "numberStripped": 120.5, |
| 151 | + "zero": false, |
| 152 | + "positive": true, |
| 153 | + "positiveOrZero": true, |
| 154 | + "negative": false, |
| 155 | + "negativeOrZero": false, |
| 156 | + "factory": { |
| 157 | + "defaultMonetaryContext": { |
| 158 | + "precision": 0, |
| 159 | + "fixedScale": false, |
| 160 | + "maxScale": 63, |
| 161 | + "amountType": "org.javamoney.moneta.Money", |
| 162 | + "providerName": null, |
| 163 | + "empty": false |
| 164 | + }, |
| 165 | + "maxNumber": null, |
| 166 | + "minNumber": null, |
| 167 | + "amountType": "org.javamoney.moneta.Money", |
| 168 | + "maximalMonetaryContext": { |
| 169 | + "precision": 0, |
| 170 | + "fixedScale": false, |
| 171 | + "maxScale": -1, |
| 172 | + "amountType": "org.javamoney.moneta.Money", |
| 173 | + "providerName": null, |
| 174 | + "empty": false |
| 175 | + } |
| 176 | + } |
| 177 | + } |
| 178 | +} |
| 179 | +``` |
| 180 | + |
| 181 | +This is not what we want!!! :fearful: What we would like to have as response is something like the following json. |
| 182 | + |
| 183 | +```json |
| 184 | +{ |
| 185 | + "idProduct": 3, |
| 186 | + "description": "Yet another product", |
| 187 | + "amount": { |
| 188 | + "amount": "120.5", |
| 189 | + "currency": "EUR" |
| 190 | + } |
| 191 | +} |
| 192 | +``` |
| 193 | + |
| 194 | +We also have the same problem in the `/product/add` endpoint, where we are forced to send a request with the payload |
| 195 | +above, the one with all the `Money` fields, in order to add a product. How can we customize the way the Jackson |
| 196 | +`ObjectMapper` serialize/deserialize `Money` instances? :thinking: We can write a custom `Module` for it. By |
| 197 | +defining a custom module we can add ad-hoc serializers and deserializers. We will define our object mapper module in |
| 198 | +a *new maven module* called `money-module`. |
| 199 | +Let's start by defining the`MoneyDeserializer`. It will give us the ability to define a `Money` instance from the data |
| 200 | +contained in a JSON that we are deserializing. |
| 201 | + |
| 202 | +```kotlin |
| 203 | +open class MoneyDeserializer : StdDeserializer<Money>(Money::class.java) { |
| 204 | + override fun deserialize(jsonParser: JsonParser, obj: DeserializationContext): Money { |
| 205 | + val node: JsonNode = jsonParser.codec.readTree(jsonParser) |
| 206 | + val amount = BigDecimal(node.get("value").asText()) |
| 207 | + val currency: String = node.get("currency").asText() |
| 208 | + |
| 209 | + return Money.of(amount, currency) |
| 210 | + } |
| 211 | +} |
| 212 | +``` |
| 213 | + |
| 214 | +The `MoneySerializer` let us defined which fields of a `Money` instance we want to write to a json. We can also the |
| 215 | +define the specific type we want to use in the json for each one of them. |
| 216 | + |
| 217 | +```kotlin |
| 218 | +open class MoneySerializer : StdSerializer<Money>(Money::class.java) { |
| 219 | + @Throws(IOException::class) |
| 220 | + override fun serialize(money: Money, jsonGenerator: JsonGenerator, serializerProvider: SerializerProvider) { |
| 221 | + jsonGenerator.writeStartObject() |
| 222 | + jsonGenerator.writeStringField("amount", money.numberStripped.toPlainString()) |
| 223 | + jsonGenerator.writeStringField("currency", money.currency.toString()) |
| 224 | + jsonGenerator.writeEndObject() |
| 225 | + } |
| 226 | +} |
| 227 | +``` |
| 228 | + |
| 229 | +Now we can add our custom serializer/deserializer to our `MoneyModule` definition. |
| 230 | + |
| 231 | +```kotlin |
| 232 | +class MoneyModule: SimpleModule() { |
| 233 | + override fun getModuleName(): String = this.javaClass.simpleName |
| 234 | + |
| 235 | + override fun setupModule(context: SetupContext) { |
| 236 | + val serializers = SimpleSerializers() |
| 237 | + serializers.addSerializer(Money::class.java, MoneySerializer()) |
| 238 | + context.addSerializers(serializers) |
| 239 | + |
| 240 | + val deserializers = SimpleDeserializers() |
| 241 | + deserializers.addDeserializer(Money::class.java, MoneyDeserializer()) |
| 242 | + context.addDeserializers(deserializers) |
| 243 | + } |
| 244 | +} |
| 245 | +``` |
| 246 | + |
| 247 | +We are now at the core of our development: how do we load our custom module into our object mapper? Well we have |
| 248 | +different options based on our use case. If you're using the *DEFAULT Spring Boot object mapper* you can just *define |
| 249 | +a new `@Bean` for the `MoneyModule`* that will be used by Spring Boot to load it(with its custom internal flow). |
| 250 | +But this is not our case: in our `ProductConfiguration` we have defined a custom `objectMapper` bean. |
| 251 | + |
| 252 | +```kotlin |
| 253 | +@Configuration |
| 254 | +class ProductConfiguration { |
| 255 | + @Bean |
| 256 | + fun productRepository(): ProductRepository = ProductRepository() |
| 257 | + |
| 258 | + @Bean |
| 259 | + @Primary |
| 260 | + fun objectMapper(): ObjectMapper = |
| 261 | + ObjectMapper() |
| 262 | + .findAndRegisterModules() |
| 263 | + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) |
| 264 | +} |
| 265 | +``` |
| 266 | + |
| 267 | +If you look closely to our `ObjectMapper` definition you can see something interesting: before returning the |
| 268 | +instance creation there is a call to the `findAndRegisterModules`. What does this method do? :thinking: It contains |
| 269 | +the core feature of Jackson `Module`s load :heart_eyes:. This method is in charge of loading external third party |
| 270 | +modules using the [Java Service Provider interfaces](https://www.baeldung.com/java-spi). |
| 271 | +This a feature of Java 6 that let (library) developer write code to load and discovery third party plugins for their |
| 272 | +library implementations that matches a specific interface. In our case Jackson uses it to load every third party |
| 273 | +implementation that matches the `Module` interface. How does it work? The `ServiceProvider` will scan the classpath |
| 274 | +searching for service definition adhering to the base interface defined for the external/third party implementation. |
| 275 | +This is done by searching for a specific file in the folders `META-INF/services` of the (maven) modules in the classpath, named with |
| 276 | +the fully qualified name of the interface loaded by the `ServiceProvider` and that contains the fully qualified name |
| 277 | +of our implementation. So in our case, to load our `MoneyModule` contained in the maven module `money-module`, we |
| 278 | +just have to add a file named `com.fasterxml.jackson.databind.Module` in the `META-INF/services` folder of the |
| 279 | +`money-module` and inside it write the fully qualified name of our `MoneyModule` implementation. |
| 280 | + |
| 281 | + |
| 282 | + |
| 283 | +That's it!!! :rocket: With the implementation above we have a custom Jackson `Module` that will be loaded by its |
| 284 | +`ObjectMapper` automatically without creating any dependencies. In this way you will be able to publish your |
| 285 | +custom serializer/deserializer as custom maven artifacts and you them in all your projects (without copy/paste them) :heart:. |
| 286 | + |
| 287 | +#### Conclusion |
| 288 | + |
| 289 | +You can find the complete source code of the example show above in this [Github repository](https://github.com/chicio/Custom-Jackson-Module). Stay tuned for new |
| 290 | +article on one of the technologies/Pattern above (Axon, CQRS, Event Sourcing, we have a lot of stuff to talk about |
| 291 | +:heart: ). |
0 commit comments