Kotlin Object Mapping - Pros & Cons

Search for a command to run...

No comments yet. Be the first to comment.
What is Ostara Ostara is a modern tool for managing and monitoring actuator-enabled microservices that aims to make the process more user-friendly and straightforward. One of our main goals with Ostara was to create a tool that just works out of the ...

The Strategy Pattern If you already know the strategy pattern just jump to "The Library" section. What is it? According to Wikipedia, the strategy pattern is a behavioral software design pattern that enables selecting an algorithm at runtime. Instead...

WHY The short answer We needed a simple to use, minimal boiler plate mapping engine, that is also flexible and supports the most advanced use cases. The long answer A few years ago we got tired of developing the same boiler plate code for CRUD and de...

The stack — Java, GWT, Mysql The Experience — 11 Years The Job — Senior Developer at Startup — Teaching some new recruits It was my 3rd year at the startup, things were going well, the company was growing. We needed more working hands, so like any co...

This is the Java version of the Kotlin article. When working with multi-layered applications, external libraries, a legacy code base or external APIs, we are often required to map between different objects or data structures. In this tutorial, we wil...

Multi-layered applications often have similar but different object models, where the data in two models may be similar but the structure and concerns of the models are different. Writing the mapping code is a tedious and error-prone task. Object mapping makes it easy to convert one model to another.
Let's say we have a REST API application with two models. One for the server and one client facing:
User (Server):
data class User(
val id: String,
val name: String,
val email: String,
// isBlocked is server side only
val isBlocked: Boolean
)
UserDTO (Client):
data class UserDTO(
val id: String,
val name: String,
val email: String
)
We have a couple of options:
We can use simple extension methods.
fun User.toUserDTO(): UserDTO {
return UserDTO(id, name, email)
}
Simple and easy, but requires additional boiler-plate code we need to maintain every time we change the model.
We will use the open source object mapping library ShapeShift for the examples.
The first step is to add the annotations to the fields we want to map.
@DefaultMappingTarget(UserDTO::class)
data class User(
@MappedField
var id: String = "",
@MappedField
var name: String = "",
@MappedField
var email: String = "",
// isBlocked is server side only and not mapped to client DTO
var isBlocked: Boolean = false
)
All that's left is to convert.
val shapeShift = ShapeShiftBuilder().build()
val user = User("xyz", "john doe", "john@email.com", false)
val userDTO = shapeShift.map<UserDTO>(user)
In some cases we can't change the data classes code (or don't want to). For these use cases we create a standalone mapper between the two classes using Kotlin DSL.
val mapper = mapper<User, UserDTO> {
User::id mappedTo UserDTO::id
User::name mappedTo UserDTO::name
User::email mappedTo UserDTO::email
}
And now the conversion. Note that we are registering the mapper to the ShapeShift instance.
val shapeShift = ShapeShiftBuilder().withMapping(mapper).build()
val user = User("xyz", "john doe", "john@email.com", false)
val userDTO = shapeShift.map<UserDTO>(user)
Seems like a lot of work for a simple conversion, right? Maybe... Let's continue to the next example.
What if I told you there is a way to remove almost if not all the mapping boiler-plate code? In many use cases we have similar classes we need to map between. This is where automatic mapping comes to the rescue.
Let's continue with the same classes.
data class User(
val id: String,
val name: String,
val email: String,
val isBlocked: Boolean
)
data class UserDTO(
val id: String,
val name: String,
val email: String
)
Using automatic mapping our mapper will be the following.
val mapper = mapper<User, UserDTO> {
autoMap(AutoMappingStrategy.BY_NAME_AND_TYPE)
}
Voila! No boiler-plate code to configure fields manually. Auto mapping also comes with advanced features for more complex use cases.
When our projects start to scale, we want to add generic functionality to improve productivity and decrease maintenance. For example:
In order for us to be able to implement these generic functionality we need to be able to convert our objects back and forth between classes.
We can use the factory method pattern to do it, simple and straightforward, but agin requires more boiler-plate code we need to maintain when we add new models.
Another option is (surprise surprise) to use object mapping.
inline fun <reified ExportModel : Any> export(model: Any) {
val shapeShift = ShapeShiftBuilder().build()
val exportModel = shapeShift.map<ExportModel>(model)
// export logic...
}
val user = User(/*...*/)
export<UserExport>(user)
That's it. Zero boiler-plate code and we got ourselves the generic functionality we know and love.
Let's take it further and add type safety to the export method. For that we will add two interfaces:
interface ExportModel {
}
interface BaseModel<EM: ExportModel> {
}
Now we will update our classes to implement the interfaces.
class UserExport: ExportModel {
// ...
}
class User: BaseModel<UserExport> {
// ...
}
And change our generic method respectively.
inline fun <reified EM : ExportModel> export(model: BaseModel<EM>) {
val shapeShift = ShapeShiftBuilder().build()
val exportModel = shapeShift.map<EM>(model)
// export logic...
}
That's it. We got ourselves type safe generic export method.
val user = User(/*...*/)
export(user)
Taking it up a notch to the next level. We have three models.
@Entity
@Table(name = "jobs")
class Job {
var id: String = ""
var name: String = ""
}
@Entity
@Table(name = "users")
class User {
var id: String = ""
var jobId: String? = null
}
class UserDTO {
var id: String = ""
var jobName: String? = null
}
We need to convert the jobId on User to jobName on UserDTO by querying the job from the DB and setting it on the DTO.
In Spring's case, you generally avoid interaction with the application context from static functions or functions on domain objects and because of that they cannot be invoked from extensions.
Object mapping to the rescue (surprise!). Let's create a custom transformer to do the heavy lifting for us.
@Component
class JobIdToNameTransformer(
private val jobDao: JobDao
) : MappingTransformer<String, String>() {
override fun transform(context: MappingTransformerContext<out String>): String? {
context.originalValue ?: return null
val job = jobDao.findJobById(context.originalValue!!)
return job.name
}
}
The complicated part is over. Now we just need to add the annotations to the User model.
@Entity
@Table(name = "users")
@DefaultMappingTarget(UserDTO::class)
class User {
@MappedField
var id: String = ""
@MappedField(transformer = JobIdToNameTransformer::class, mapTo = "jobName")
var jobId: String? = null
}
That's all. The great part is that we can use the same transformer every time we need to convert a job id to job name on any model.
val shapeShift = ShapeShiftBuilder().build()
val user = User(/*...*/)
val userDTO = shapeShift.map<UserDTO>(user)
A full spring example code is available here.
Each conversion method has its own pros & cons.
Like most things in development, there is no right answer here. The answer is that it depends on the project. For small simple projects extension methods are more than enough to convert classes, but for large enterprise projects using an object mapper can give the freedom to achieve better architecture and more generic code that can keep moving forward with large codebase and lots of models.