To convert some classes to others, it is convenient to use libraries for automatic conversion instead of writing mappers manually. In my opinion, one of the best libraries for this is MapStruct
Its advantage is that errors during conversion, for example, if the format does not match, can be shown when compiling the project, and not at runtime, for this we create
import org.mapstruct.MapperConfig import org.mapstruct.ReportingPolicy @MapperConfig(unmappedTargetPolicy = ReportingPolicy.ERROR) interface StrictMapperConfig
and use it further. I will use the base class BaseMapper
abstract class BaseMapper<From, To> { abstract fun map(from: From): To open fun mapList(fromList: List<From>): List<To> { return fromList.mapTo(ArrayList(fromList.size), this::map) } }
NOTE: If one of the models to be converted has only one field, MapStruct will throw an error
error: Unmapped target property: "copy". public abstract com._.To map(@org.jetbrains.annotations.NotNull
A similar error can occur, for example, when classes inherit interfaces that are not compatible with MapStruct, or in some other cases.
WARNING, MapStruct, as is clear from the use of annotations, generates code for conversion, and if you use variable names in the is… format, the mapper will return an error due to a conflict in logic. The best solution is to rename the variable, there are other solutions on stackoverflow.
WARNING, MapStruct cannot handle nullable and non-nullable values normally, make sure that the variable type is the same in the initial and final class
WARNING, if the interface marked with the Mapper annotation assumes normal conversion functions, use an abstract class, not an interface, when using interface functions, there is a high probability of a compilation error
The simplest conversion option, when the classes are completely identical
data class To( val one: String, val two: String, ) data class From( val one: String, val two: String, )
import org.mapstruct.Mapper import org.mapstruct.factory.Mappers class SomeModelMapper : BaseMapper<From, To>() { @Mapper(config = StrictMapperConfig::class) fun interface ModelMapper { fun map(from: From): To } override fun map(from: From): To { return Mappers.getMapper(ModelMapper::class.java).map(from) } }
If the name of one or more fields is different, add a Mapping annotation, or multiple annotations if necessary.
data class To( val oneNewName: String, val twoNewName: String, ) data class From( val one: String, val two: String, )
import org.mapstruct.Mapper import org.mapstruct.factory.Mappers class SomeModelMapper : BaseMapper<From, To>() { @Mapper(config = StrictMapperConfig::class) fun interface ModelMapper { @Mapping(source = "one", target = "oneNewName") @Mapping(source = "two", target = "twoNewName") fun map(from: From): To } override fun map(from: From): To { return Mappers.getMapper(ModelMapper::class.java).map(from) } }
Here is an example of a transformation if you need to replace one of the fields, for example they are of different types
data class To( val one: List<String>, val two: String, ) data class From( val one: String, val two: String, )
import org.mapstruct.Mapper import org.mapstruct.factory.Mappers class SomeModelMapper : BaseMapper<From, To>() { @Mapper(config = StrictMapperConfig::class) abstract class ModelMapper { abstract fun map(from: From): To fun mapOne(one: String): List<String> { return listOf(one) } } override fun map(from: From): To { return Mappers.getMapper(ModelMapper::class.java).map(from) } }
you can also use the Named annotation
data class To( val one: List<String>, val two: String, ) data class From( val one: String, val two: String, )
import org.mapstruct.Mapper import org.mapstruct.factory.Mappers class SomeModelMapper : BaseMapper<From, To>() { companion object { private const val MAP_ONE = "MAP_ONE" } @Mapper(config = StrictMapperConfig::class) abstract class ModelMapper { @Mapping(target = "one", source = "one", qualifiedByName = [MAP_ONE]) abstract fun map(from: From): To @Named(MAP_ONE) fun mapOne(one: String): List<String> { return listOf(one) } } override fun map(from: From): To { return Mappers.getMapper(ModelMapper::class.java).map(from) } }
To reuse logic, you can split the transformation into different classes/interfaces
data class To( val one: String, val two: List<ToItem>, ) data class ToItem( val three: List<String>, val four: String, ) data class From( val one: String, val two: List<FromItem>, ) data class FromItem( val three: String, val four: String, )
import org.mapstruct.Mapper import org.mapstruct.factory.Mappers class SomeModelMapper : BaseMapper<From, To>() { @Mapper(config = StrictMapperConfig::class, uses = [ItemMapper::class]) fun interface ModelMapper { fun map(from: From): To } @Mapper(config = StrictMapperConfig::class) abstract class ItemMapper { fun mapThree(three: String): List<String> { return listOf(three) } } override fun map(from: From): To { return Mappers.getMapper(ModelMapper::class.java).map(from) } }
you can, on the contrary, call an abstract function from a regular function, if, for example, you need to transform nested sheets with other classes whose contents are the same
data class To( val one: String, val two: List<ToItem>, ) data class ToItem( val three: String, val four: String, ) data class From( val one: String, val two: List<FromItem>, ) data class FromItem( val three: String, val four: String, )
import org.mapstruct.Mapper import org.mapstruct.factory.Mappers class SomeModelMapper : BaseMapper<From, To>() { @Mapper(config = StrictMapperConfig::class) abstract class ModelMapper { fun map(from: From): To { return with(from) { To( one = one, two = two.map(::mapItem), ) } } abstract fun mapItem(two: FromItem): ToItem } override fun map(from: From): To { return Mappers.getMapper(ModelMapper::class.java).map(from) } }
By analogy, you can create more complex structures using abstract and regular functions in an abstract class with the Mapper annotation, while respecting the names of variables and methods. MapStruct is generally a rather capricious library with a lot of nuances, but of those that I tried to use, it turned out to be the safest in terms of errors during operation.
I will give another interesting example, when the model in the text field contains XML that needs to be parsed into a class, for this I will use the org.simpleframework.xml library
https://javadoc.io/doc/org.simpleframework/simple-xml/latest/index.html
implementation 'org.simpleframework:simple-xml:2.7.1'
In the example, for convenience, in the final class I made 2 nested classes – with data from the original class and data from XML, but the structure can be any
<data> <one>text one</one> <two>text two</two> </data>
data class To( val data: ToData?, val xml: ToXML?, ) data class ToData( val one: String?, val two: String?, ) data class ToXML( val one: String?, val two: String?, ) data class From( val one: String?, val two: String?, val xml: String?, )
import org.simpleframework.xml.Element import org.simpleframework.xml.Root @Root(name = "data") data class FromXML( @field:Element(name = "one", required = false) var one: String? = null, @field:Element(name = "two", required = false) var two: String? = null, )
import org.mapstruct.Mapper import org.mapstruct.factory.Mappers import org.koin.core.component.inject import org.koin.core.component.KoinComponent import org.simpleframework.xml.core.Persister class SomeModelMapper: BaseMapper<From, To>(), KoinComponent { private val serializer: Persister by inject() @Mapper(config = StrictMapperConfig::class) interface ModelMapper { fun mapData(from: From): ToData fun mapXML(from: FromXML): ToXML } override fun map(from: From): To { val data = Mappers.getMapper(FromXML::class.java).mapJSON(from) return try { val xml = serializer.read(FromXML::class.java, from.xml!!)!! To( data = data, xml = Mappers.getMapper(ModelMapper::class.java).mapXML(xml), ) } catch (e: Exception) { logError("parse xml exception: ${e.message}", e, TAG) To( data = data, xml = null, ) } } }