DEV Community

Cover image for Kotlin Object Mapping - Pros & Cons
Aviv Mor for krud.dev

Posted on • Updated on

Kotlin Object Mapping - Pros & Cons

What is Object Mapping?

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.

Simple Example

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
)
Enter fullscreen mode Exit fullscreen mode

UserDTO (Client):

data class UserDTO(
    val id: String,
    val name: String,
    val email: String
)
Enter fullscreen mode Exit fullscreen mode

Conversion

We have a couple of options:

Extension Methods

We can use simple extension methods.

fun User.toUserDTO(): UserDTO {
    return UserDTO(id, name, email)
}
Enter fullscreen mode Exit fullscreen mode

Simple and easy, but requires additional boiler-plate code we need to maintain every time we change the model.

Object Mapping

We will use the open source object mapping library ShapeShift for the examples.

Annotations

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
)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

DSL

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
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Seems like a lot of work for a simple conversion, right? Maybe... Let's continue to the next example.

Auto Mapping

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
)
Enter fullscreen mode Exit fullscreen mode

Using automatic mapping our mapper will be the following.

val mapper = mapper<User, UserDTO> {
    autoMap(AutoMappingStrategy.BY_NAME_AND_TYPE)
}
Enter fullscreen mode Exit fullscreen mode

Voila! No boiler-plate code to configure fields manually. Auto mapping also comes with advanced features for more complex use cases.

Generic Example

When our projects start to scale, we want to add generic functionality to improve productivity and decrease maintenance. For example:

  • Generic CRUD controller for entities.
  • Generic export to excel/pdf/...

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)
Enter fullscreen mode Exit fullscreen mode

That's it. Zero boiler-plate code and we got ourselves the generic functionality we know and love.

Type Safety

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> {
}
Enter fullscreen mode Exit fullscreen mode

Now we will update our classes to implement the interfaces.

class UserExport: ExportModel {
    // ...
}

class User: BaseModel<UserExport> {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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...
}
Enter fullscreen mode Exit fullscreen mode

That's it. We got ourselves type safe generic export method.

val user = User(/*...*/)
export(user)
Enter fullscreen mode Exit fullscreen mode

Spring Example

Taking it up a notch to the next level. We have three models.

  • Job - DB entity.
  • User - DB entity.
  • UserDTO - Client model.
@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
}
Enter fullscreen mode Exit fullscreen mode

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 
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

A full spring example code is available here.

Pros & Cons

Each conversion method has its own pros & cons.

Extension Methods

Pros

  • Simple implementation.
  • Clear code - no annotations "magic".

Cons

  • Lots of boiler plate code.
  • Limited functionality.
  • Does not scale well.

Object Mapper

Pros

  • Little to none boiler-plate code.
  • Auto mapping.
  • Generic code.
  • Advanced features.
  • Code reuse.

Cons

  • Requires to learn new library.
  • Annotations "magic".

Summary

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.

Top comments (0)