Object Mapping advanced features & QoL with Kotlin

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...

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 will check out some Object Mapping libraries advanced features to simplify this task while saving development and maintenance time.
In our examples we will use the library ShapeShift. It's a light-weight object mapping library for Kotlin/Java with lots of cool features.
We will start with a bang. Auto mapping can and will save you lots of time and boiler-plate code. Some applications require manual mapping between objects, but most applications will save tons of time working on boring boiler-plate by just using this one feature. And it gets even better, with ShapeShift's default transformers we can even use automatic mapping between different data types.
Let's start with a simple example for auto mapping. We have our two objects, imagine they could also have tens, hundreds, or even thousands (for the crazy people here) of fields.
class User {
var id: String = ""
var name: String? = null
var email: String? = null
var phone: String? = null
}
class UserDTO {
var id: String = ""
var name: String? = null
var email: String? = null
var phone: String? = null
}
We want to map all the field from User to UserDTO. Using auto mapping we don't need to write any boiler-plate code. The mapper will be defined as follow:
val mapper = mapper<User, UserDTO> {
autoMap(AutoMappingStrategy.BY_NAME_AND_TYPE)
}
Voila! All the fields will be mapped automatically without any manual boiler-plate code.
In this example we will use the power of default transformers to take auto mapping even further.
class User {
var id: String = ""
var name: String? = null
var birthDate: Date? = null
}
class UserDTO {
var id: String = ""
var fullName: String? = null
var birthDate: Long? = null
}
Note that the types of the birthDate field are different in the source and destination classes. But using the power of default transformers we can still use auto mapping here.
val mapper = mapper<User, UserDTO> {
autoMap(AutoMappingStrategy.BY_NAME)
}
We changed the auto mapping strategy to BY_NAME so it will map fields also with different types. Now we need to register a default transformer to the ShapeShift instance in order for it to know how to transform Date to Long.
val shapeShift = ShapeShiftBuilder()
.withTransformer(DateToLongMappingTransformer(), true)
.build()
We can also add manual mapping on top of the auto mapping in order to add/change behavior. The source and destination classes have different names for the name field so we will add manual mapping for it.
val mapper = mapper<User, UserDTO> {
autoMap(AutoMappingStrategy.BY_NAME)
User::name mappedTo UserDTO::fullName
}
Auto mapping is great for use cases that does not require specific mapping. It helps reduce the amount manual boiler-plate code needed to configure mapping and also helps you keep your sanity.
Transformers are very useful feature that allows you to transform the type/value of a field to a different type/value when mapping a field.
Some use cases we have been using widely:
We will start with a simple transformer example. Date-to-Long and Long-to-Date transformers:
class DateToLongMappingTransformer : MappingTransformer<Date, Long> {
override fun transform(context: MappingTransformerContext<out Date>): Long? {
return context.originalValue?.time
}
}
class LongToDateMappingTransformer : MappingTransformer<Long, Date> {
override fun transform(context: MappingTransformerContext<out Long>): Date? {
context.originalValue ?: return null
return Date(context.originalValue)
}
}
All we need to do now is to register them.
val shapeShift = ShapeShiftBuilder()
.withTransformer(DateToLongMappingTransformer(), true) // "true" is optional, we are registering the transformers as default transformers, more on that later.
.withTransformer(LongToDateMappingTransformer(), true)
.build()
That's it! We can now use the transformers when mapping objects.
class User {
var id: String = ""
var name: String? = null
var birthDate: Date? = null
}
class UserDTO {
var id: String = ""
var name: String? = null
var birthDate: Long? = null
}
val mapper = mapper<User, UserDTO> {
User::id mappedTo UserDTO::id
User::name mappedTo UserDTO::name
User::birthDate mappedTo UserDTO::birthDate withTransformer DateToLongMappingTransformer::class // We don't have to state the transformer here because it is a default transformer
}
In some use cases we want to transform the value but we don't need a reusable transformer and we don't want to create a class just for a one time use.
Inline transformers for the rescue! Inline transformers allow to transform the value without the need to create and register and transformer.
val shapeShift = ShapeShiftBuilder()
.withMapping<Source, Target> {
// Map birthDate to birthYear with a transformation function
Source::birthDate mappedTo Target::birthYear withTransformer { (originalValue) ->
originalValue?.year
}
}
.build()
Transformers also allow us to do transformations with the DB or other data sources.
In this example we will use the power of the Spring Boot integration to create transformers with DB access.
We have three models:
class Job {
var id: String = ""
var name: String = ""
}
class User {
var id: String = ""
var jobId: String? = null
}
class UserDTO {
var id: String = ""
var jobName: String? = null
}
We want 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.
We will use a ShapeShift's Spring integration to create a component as a transformer to access our DAO bean.
@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
}
}
All that's left to do is to use this transformer in our mapping.
val mapper = mapper<User, UserDTO> {
User::id mappedTo UserDTO::id
User::jobId mappedTo UserDTO::jobName withTransformer JobIdToNameTransformer::class
}
Another bonus of using transformers is their reusability. In some use cases, We could create more generic transformers that will have application-wide usage.
When registering transformers you can indicate wether a transformer is a default transformer. A default transformer of types \ is used when you map a field of type \ to field of type \ without specifying a transformer to be used.
As we already seen, default transformers are useful for recurring transformations and especially for automatic mapping.
What if we want to map from/to fields that available inside a field which is an object? We can even do that, easily.
In order to access child classes we can use the .. operator. Let's look at the following example.
class From {
var child: Child = Child()
class Child {
var value: String?
}
}
class To {
var childValue: String?
}
We want to map the value field in Child class inside the From class to the childValue field in the To class. We will create a mapper with the .. operator.
val mapper = mapper<From, To> {
From::child..From.Child::value mappedTo To::childValue
}
Let's take it one step further with multi level depth.
class From {
var grandChildValue: String?
}
class To {
var child: Child = Child()
class Child {
var grandChild: GrandChild = GrandChild()
}
class GrandChild {
var value: String?
}
}
To access the grand child field we just use the .. operator twice.
val mapper = mapper<From, To> {
From::grandChildValue mappedTo To::child..To.Child::grandChild..To.GrandChild::value
}
Conditions allow us to add a predicate to a specific field mapping to determine whether this field should be mapped.
Using this feature is as easy as creating a condition.
class NotBlankStringCondition : MappingCondition<String> {
override fun isValid(context: MappingConditionContext<String>): Boolean {
return !context.originalValue.isNullOrBlank()
}
}
And adding the condition to the desired field mapping.
data class SimpleEntity(
val name: String
)
data class SimpleEntityDisplay(
val name: String = ""
)
val mapper = mapper<SimpleEntity, SimpleEntityDisplay> {
SimpleEntity::name mappedTo SimpleEntityDisplay::name withCondition NotBlankStringCondition::class
}
Like transformers, conditions can also be added inline using a function.
val mapper = mapper<SimpleEntity, SimpleEntityDisplay> {
SimpleEntity::name mappedTo SimpleEntityDisplay::name withCondition {
!it.originalValue.isNullOrBlank()
}
}
This specific feature receives lots of hate because it breaks the separation of concerns principle. Agreed, this could be an issue in some applications, but in some use cases where all objects are part of the same application it can also be very useful to configure the mapping logic on top of the object. Check out the documentation and decide for yourself.
Object mapping libraries are not the solution for every application. For small, simple applications using boiler-plate mapping functions are more than enough. But, when developing larger, more complex applications, object mapping libraries can take your code to the next level, saving you development and maintenance time. All of these while reducing the amount of boiler-plate code and overall improving the development experience.
On a personal note, I used to work with manual mapping functions and was ok with it. It was "just" some simple lines of code. After upgrading our applications to use object mapping as part of our "boiler-plate free" framework (We will discuss that framework at a later time), I can't go back. Now we spend more time on what's important and interesting and almost no time on boring boiler-plate code.