Avoiding Boilerplate Code with MapStruct, Spring Boot, and Kotlin
By: Antonello Zanini
Tags: Spring Boot, Tutorial, Kotlin, Dto, Mapstruct

Photo by Christopher Gower on Unsplash

Building Spring Boot Rest Web Services might become cumbersome, especially when domain model objects need to be converted in many different DTOs (Data Transfer Object) and vice versa. Writing mapping manually is boring and involves boilerplate code. Is it possible to avoid it?

Yes, it is! Thanks to MapStruct! MapStruct is a Java annotation processor for the generation of type-safe and performant mappers for Java bean classes.

Integrating MapStruct with Spring Boot in Java is well documented (Automating Code with MapStruct), but what if we wanted to use Kotlin?

Let’s see how MapStruct can be integrated into a Spring Boot and Kotlin project.

1. Gradle dependency

Let’s add the Kapt compiler plugin and the MapStruct processor dependencies in build.gradle.kts.

plugins {

kotlin("kapt") version "1.3.72"

} dependencies {

kapt("org.mapstruct:mapstruct-processor:1.3.1.Final")

}

mapstruct-processor is required to generate the mapper implementation during build-time, while kapt is the Kotlin Annotation Processing Tool, and it is used to reference the generated code from Kotlin.

Now, our build.gradle.kts file looks like this:

2. Creating domain model objects and DTOs

We are going to create two domain model classes: Author and Book.

Now, we need to create the corresponding DTO classes.

What if we wanted a special DTO for Author? Maybe, we might be interested only in the first book written by an author, and not in their birth date. Let’s build a special DTO to accomplish this goal.

3. Defining MapStruct mappers

MapStruct is not based on magic and, in order to map domain model objects in DTO objects, requires the definition of one or more mappers.

Each mapper is an interface or an abstract class and must be annotated with @Mapper annotation. This causes the MapStruct code generator to create an implementation of the mapper interface or abstract class during build-time. Hence, implementation is not required. This is where the magic begins and allows us to avoid boilerplate code.

By default, in the generated method implementations all readable properties from the source type (e.g. Author ) will be mapped into the corresponding property in the target type (e.g. AuthorDto ).

Let’s see how a mapper can be defined.

The componentModel attribute is set to “spring” to force the MapStruct processor to generate a singleton Spring bean mapper that can be injected directly where need it.

If you are interested in studying what the MapStruct processor produced at build-time, you can find the implementation classes in build/generated/source/kapt/.

Let’s see an example.

What if we need a custom mapping logic, as in the SpecialAuthor example?

We have to tell the MapStruct processor that a specific field of the source object has to be mapped through a special method, containing the mapping logic.

In order to make MaStruct build a correct implementation class, a mapper abstract class is required. In fact, Kotlin default method implementation in interfaces seems to be ignored by the MapStruct processor.

In the expression field we need to use Java-like setters, since Java is required.

4. Putting it all together

Let’s create a controller and test our work.

The two mappers are injected into the controller, which uses them to produce the required DTO classes. These classes are then converted into JSON by Jackson and used to build the response of each API.

Let’s generate some authors and books.

@Component

class DataSource {



val data : MutableList<Author> = ArrayList()



init {

val book1 = Book(

1,

"Book 1",

GregorianCalendar(2018, 10, 24).time

)



val book2 = Book(

2,

"Book 2",

GregorianCalendar(2010, 7, 12).time

)



val book3 = Book(

3,

"Book 3",

GregorianCalendar(2011, 3, 8).time

)



val author1 = Author(

1,

"John",

"Smith",

GregorianCalendar(1967, 1, 2).time,

listOf(book1, book2, book3)

)



val book4 = Book(

4,

"Book 4",

GregorianCalendar(2012, 12, 9).time

)



val book5 = Book(

5,

"Book 5",

GregorianCalendar(2017, 9, 22).time

)



val author2 = Author(

2,

"Emma",

"Potter",

GregorianCalendar(1967, 1, 2).time,

listOf(book5, book4)

)



data.addAll(listOf(author1, author2))

}

}

Then, these will be the responses of each API:

[

{

"id":1,

"name":"John",

"surname":"Smith",

"birth":"02/01/1967",

"books":[

{

"id":1,

"title":"Book 1",

"release":"11/23/2018"

},

{

"id":2,

"title":"Book 2",

"release":"08/11/2010"

},

{

"id":3,

"title":"Book 3",

"release":"04/07/2011"

}

]

},

{

"id":2,

"name":"Emma",

"surname":"Potter",

"birth":"02/01/1967",

"books":[

{

"id":5,

"title":"Book 5",

"release":"10/21/2017"

},

{

"id":4,

"title":"Book 4",

"release":"01/08/2013"

}

]

}

]

{

"id":1,

"name":"John",

"surname":"Smith",

"birth":"02/01/1967",

"books":[

{

"id":1,

"title":"Book 1",

"release":"11/23/2018"

},

{

"id":2,

"title":"Book 2",

"release":"08/11/2010"

},

{

"id":3,

"title":"Book 3",

"release":"04/07/2011"

}

]

}

{

"name":"John",

"surname":"Smith",

"firstBook":{

"id":2,

"title":"Book 2",

"release":"08/11/2010"

}

}

Extras

DTOs can also be used to define a specific (de)serialization layer in a multi-layered architecture as described here: Designing a Multi-Layered Architecture for Building RESTful Web Services With Spring Boot and Kotlin