---
title: "Невеликий мануал з MapStruct"
url: "https://romankryvolapov.com/uk/mapstruct-manual/"
description: "Практичні прийоми MapStruct у Kotlin: мапінг полів із різними іменами, @Named, перевикористання логіки та обхід типових помилок компіляції."
language: uk
updated: 2026-01-21
---
Для перетворення одних класів на інші зручно використовувати бібліотеки для автоматичного перетворення замість того, щоб писати мапери вручну. На мою думку, одна з найкращих бібліотек для цього 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)

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