---
title: "Кратко ръководство за MapStruct"
url: "https://romankryvolapov.com/bg/mapstruct-manual/"
description: "Практически похвати с MapStruct в Kotlin: маппинг на полета с различни имена, @Named, преизползване на логиката и заобикаляне на типични грешки при компилация."
language: bg
published: 2024-03-13
updated: 2026-11-13
---
За преобразуване на едни класове в други е удобно да се използват библиотеки за автоматично преобразуване, вместо мапърите да се пишат на ръка. Според мен една от най-добрите библиотеки за това е MapStruct

[https://mapstruct.org](https://mapstruct.org)

## Настройка на MapStruct

Предимството ѝ е, че грешките при преобразуване, например при несъвпадение на формата, могат да се показват още при компилацията на проекта, а не по време на изпълнение; за целта създаваме

```kotlin
import org.mapstruct.MapperConfig
import org.mapstruct.ReportingPolicy


@MapperConfig(unmappedTargetPolicy = ReportingPolicy.ERROR)
interface StrictMapperConfig
```

и го използваме по-нататък. Аз ще използвам базовия клас BaseMapper

```kotlin
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)
    }
}
```

## Важни забележки

ВНИМАНИЕ: ако в един от моделите, които трябва да се преобразуват, има само едно поле, MapStruct връща грешка

```text
error: Unmapped target property: "copy". public abstract com._.To map(@org.jetbrains.annotations.NotNull
```

Подобна грешка може да възникне например и когато класове наследяват интерфейси, несъвместими с MapStruct, или в някои други случаи.

ВНИМАНИЕ: MapStruct, както личи от използването на анотации, генерира код за преобразуването и ако използвате имена на променливи във формат is…, мапърът ще върне грешка заради конфликт в логиката. Най-доброто решение е да преименувате променливата; има и други решения в stackoverflow.

ВНИМАНИЕ: MapStruct не умее да обработва нормално nullable и не-nullable стойности – следете типът на променливата в началния и в крайния клас да е един и същ

ВНИМАНИЕ: ако в интерфейса, отбелязан с анотацията Mapper, се предполагат обикновени функции за преобразуване, използвайте абстрактен клас, а не интерфейс – при използване на функции на интерфейса вероятността за грешка при компилация е голяма

## Най-простият вариант на преобразуване

Най-простият вариант на преобразуване, когато класовете напълно съвпадат

```kotlin
data class To(
    val one: String,
    val two: String,
)

data class From(
    val one: String,
    val two: String,
)
```

```kotlin
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)
    }
}
```

## Маппинг с различни имена на полетата

Ако името на едно или няколко полета се различава, добавяме анотацията Mapping или няколко анотации, ако е необходимо

```kotlin
data class To(
    val oneNewName: String,
    val twoNewName: String,
)

data class From(
    val one: String,
    val two: String,
)
```

```kotlin
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)
    }
}
```

## Преобразуване на полета от различни типове

А ето пример за преобразуване, ако е необходимо да се замени едно от полетата, например когато са от различен тип

```kotlin
data class To(
    val one: List<String>,
    val two: String,
)

data class From(
    val one: String,
    val two: String,
)
```

```kotlin
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)
    }
}
```

## Използване на анотацията Named

Можете да използвате и анотацията Named

```kotlin
data class To(
    val one: List<String>,
    val two: String,
)

data class From(
    val one: String,
    val two: String,
)
```

```kotlin
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)
    }
}
```

## Преизползване на логиката

За да преизползваме логиката, можем да разделим преобразуването на различни класове / интерфейси

```kotlin
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,
)
```

```kotlin
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)
    }
}
```

## Извикване на абстрактна функция от обикновена

Може и обратното – от обикновена функция да се извиква абстрактна, ако например трябва да се преобразуват вложени списъци с други класове, чието съдържание е еднакво

```kotlin
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,
)
```

```kotlin
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)
    }
}
```

По аналогия могат да се създават и по-сложни конструкции, като в абстрактния клас с анотацията Mapper се използват абстрактни и обикновени функции, като същевременно се спазват имената на променливите и методите. MapStruct като цяло е доста капризна библиотека с много нюанси, но от тези, които съм пробвал да използвам, се оказа най-безопасната откъм грешки при работа.

## Пример с парсване на XML

Ще приведа още един интересен пример, когато в модела в текстово поле има XML, който трябва да се парсне в клас; за целта ще използвам библиотеката org.simpleframework.xml

[https://javadoc.io/doc/org.simpleframework/simple-xml/latest/index.html](https://javadoc.io/doc/org.simpleframework/simple-xml/latest/index.html)

```groovy
implementation 'org.simpleframework:simple-xml:2.7.1'
```

В примера за удобство в крайния клас направих 2 вложени класа – с данни от оригиналния клас и с данни от XML, но структурата може да бъде произволна

```xml
<data>
    <one>text one</one>
    <two>text two</two>
</data>
```

```kotlin
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?,
)
```

```kotlin
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,
)
```

```kotlin
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(ModelMapper::class.java).mapData(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 ](https://t.me/RomanKryvolapov)
