In Java, I used MapStruct in many projects. When you start writing in a new language (in this case Kotlin), there is a natural desire to transfer some of the practices.
Moreover, MapStruct itself works quite well with Kotlin. It only generates Java code. Because of this, there are limitations (after all, Kotlin contains more constructs than Java – the same named parameters and functions without classes). It is especially sad that null typing disappears (in Java, any variable can be null at the language level).
There is an obvious desire to do the same, but already fully adapted to Kotlin. I even started doing it (and wrote a lot of it), but in the end I decided not to publish it and not to use it myself.
What are the problems of MapStruct and the like?
- During the development of the application, from time to time these magical conversion functions go sideways (with errors) - it is better when it is typed it will break and an informed decision will be made how to change it.
- Quite often there is no typing – both types and pieces of Java code are passed in strings
- Some kind of regular DSL that you need to additionally learn and remember if you haven’t used it for a while
- There is no saving time on tests - they must be written, because it is not so rare that they really help fix bugs
- From time to time it doesn’t work – you need to go into the code and figure out why it doesn’t generate what you need
- Compilation time increases, and usually for all modules, which is always unpleasant
All these problems are unrelated to Kotlin and almost unrelated to a specific implementation. MapStruct is essentially syntactic sugar, which is much less needed in the case of Kotlin. Let’s look at the code.
The original example from the MapStruct website in Java:
public class Car {
private String make;
private int numberOfSeats;
private CarType type;
//constructor, getters, setters etc.
}
public class CarDto {
private String make;
private int seatCount;
private String type;
//constructor, getters, setters etc.
}
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto carToCarDto(Car car);
}
// CarDto dto = CarMapper.INSTANCE.carToCarDto(car)
And the same thing, but in Kotlin (and without abbreviations as it was in Java and with full typing, and not in strings):
data class Car(
val make: String,
val numberOfSeats: Int,
val type: CarType,
)
data class CarDto(
val make: String,
val seatCount: Int,
val type: String,
) {
compation object{
fun from(from: Car) = CarDto(
make = from.make,
seatCount = from.numberOfSeats,
type = from.type.toString()
)
}
}
val dto = CarDto.from(car)
Moreover, the conversion function can also be added to the source class:
data class Car(
val make: String,
val numberOfSeats: Int,
val type: CarType,
) {
fun toDto() = CarDto(
make = make,
seatCount = numberOfSeats,
type = type.toString()
)
}
data class CarDto(
val make: String,
val seatCount: Int,
val type: String,
)
val dto = car.toDto()
Just as a function:
fun car2dto(from: Car) = CarDto(
make = from.make,
seatCount = from.numberOfSeats,
type = from.type.toString()
)
val dto = car2dto(car)
Or as an extension function:
fun Car.toDto() = CarDto(
make = make,
seatCount = numberOfSeats,
type = type.toString()
)
val dto = car.toDto()
So you can choose an approach depending on how the application is organized.
If I don’t write something else, I will definitely keep you informed).