Skip to content

Small manual on MapStruct

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

https://mapstruct.org

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,
            )
        }
    }
}

Copyright: Roman Kryvolapov