Skip to content
This repository was archived by the owner on Jan 19, 2025. It is now read-only.

Commit 0b57949

Browse files
committed
Completed new post 🚀
1 parent c1cbd52 commit 0b57949

File tree

2 files changed

+291
-0
lines changed

2 files changed

+291
-0
lines changed
5.04 KB
Loading
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
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+
![contract testing pact](../images/posts/module-jackson.jpg "custom module jackson findAndRegisterModules")
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

Comments
 (0)