Collection Mapping¶
Kraft maps collection properties element by element: List, Set, Collection, Iterable, Array, MutableList, MutableSet and MutableCollection. When the source and target have collection properties with different element types, Kraft automatically generates both a parent mapper and a child mapper for the element type pair.
How It Works¶
When a source class has List<SourceItem> and the target has List<TargetItem> (with different element types), Kraft generates:
- An extension function for the parent class mapping
- A child extension function for
SourceItem -> TargetItem - A
.map { ... }call inside the parent mapper that invokes the child mapper
List Example¶
data class ItemSource(val id: Int, val label: String)
data class OrderSource(val ref: String, val items: List<ItemSource>)
data class ItemDto(val id: Int, val label: String)
data class OrderDto(val ref: String, val items: List<ItemDto>)
@MapConfig(source = OrderSource::class, target = OrderDto::class)
object OrderMapper
Generated code:
fun OrderSource.toOrderDto(): OrderDto = OrderDto(
ref = this.ref,
items = this.items.map { it.toItemDto() }
)
fun ItemSource.toItemDto(): ItemDto = ItemDto(
id = this.id,
label = this.label
)
Set Example¶
For Set properties, Kraft appends .toSet() after the .map { ... } call:
data class ItemSource(val id: Int, val label: String)
data class OrderSource(val ref: String, val items: Set<ItemSource>)
data class ItemDto(val id: Int, val label: String)
data class OrderDto(val ref: String, val items: Set<ItemDto>)
@MapConfig(source = OrderSource::class, target = OrderDto::class)
object OrderMapper
Generated code:
fun OrderSource.toOrderDto(): OrderDto = OrderDto(
ref = this.ref,
items = this.items.map { it.toItemDto() }.toSet()
)
Supported Collection Types¶
Source and target may each be any of these types, and they need not be the same one:
| Type | Appended after .map { ... } |
|---|---|
List, Collection, Iterable |
nothing |
Set |
.toSet() |
Array |
.toTypedArray() |
MutableList, MutableCollection |
.toMutableList() |
MutableSet |
.toMutableSet() |
Generated: items = this.items.map { it.toItemDto() }.toTypedArray()
Converting between collection types¶
When the source and target types differ, Kraft converts as long as every element survives:
| Source | Target | Generated |
|---|---|---|
Array<ItemSource> |
List<ItemDto> |
this.items.map { it.toItemDto() } |
Set<ItemSource> |
List<ItemDto> |
this.items.map { it.toItemDto() } |
Iterable<ItemSource> |
MutableCollection<ItemDto> |
this.items.map { it.toItemDto() }.toMutableList() |
Set<ItemSource> |
MutableSet<ItemDto> |
this.items.map { it.toItemDto() }.toMutableSet() |
Array<String> |
List<String> |
this.tags.toList() |
List<String> |
Collection<String> |
this.tags (already a supertype, no copy) |
Any source type converts into List, Collection, Iterable, Array, MutableList and MutableCollection. Only a Set or MutableSet converts into a Set or MutableSet.
The Set rule¶
A List, Array or other collection that may hold duplicates is never converted into a Set or MutableSet, because the duplicates would be dropped without a trace. Kraft fails compilation with KRAFT-E063:
[KRAFT-E063] Cannot map property 'items': converting List<ItemSource> to Set<ItemDto> would drop duplicate elements.
Change the target to List<ItemDto>, or map it with @MapUsing and call .toSet() explicitly.
The same check applies to the reverse mapper that @MapReverse generates: a Set<ItemSource> to List<ItemDto> mapper compiles, but its reverse would turn a List into a Set and is refused.
Deprecated nested-mapping annotations¶
@MapNested and @MapConfig(nestedMappings = ...) are deprecated and stay frozen at their 0.13 behaviour: they work only for List and Set of the same kind, with the output they always had (including the existing List to Set conversion through an explicit NestedMapping with a FieldMapping rename, which is not checked by E063). The other collection types and conversions on this page come from auto-detection and @MapField / FieldMapping renames only.
Auto-Detection¶
Collection nested mapping follows the same auto-detection rules as regular nested mapping:
- The source and target properties must share the same name
- The element types must differ (if both sides use the same element type, no child mapper is generated)
- Both element types must be concrete classes with a primary constructor
When the element types are identical, the collection is assigned directly:
data class Item(val id: Int)
data class OrderSource(val ref: String, val items: List<Item>)
data class OrderDto(val ref: String, val items: List<Item>)
@MapConfig(source = OrderSource::class, target = OrderDto::class)
object OrderMapper
Generated: items = this.items (no .map call needed).
Multiple Collection Properties¶
Each collection property is handled independently:
data class AuthorSource(val name: String)
data class CommentSource(val text: String)
data class PostSource(val title: String, val authors: List<AuthorSource>, val comments: List<CommentSource>)
data class AuthorDto(val name: String)
data class CommentDto(val text: String)
data class PostDto(val title: String, val authors: List<AuthorDto>, val comments: List<CommentDto>)
@MapConfig(source = PostSource::class, target = PostDto::class)
object PostMapper
Generated:
fun PostSource.toPostDto(): PostDto = PostDto(
title = this.title,
authors = this.authors.map { it.toAuthorDto() },
comments = this.comments.map { it.toCommentDto() }
)
Nullable Collections¶
Kraft handles nullable collection types with safe-call operators and fallback values.
Nullable source, nullable target¶
List<Source>? on source and List<Target>? on target uses a safe call:
Generated: items = this.items?.map { it.toItemDto() }
Nullable source, non-null target¶
List<Source>? on source and List<Target> on target adds an emptyList() fallback:
Generated: items = this.items?.map { it.toItemDto() } ?: emptyList()
Set equivalents¶
For Set collections, nullable sources produce ?.toSet() chaining:
Generated: items = this.items?.map { it.toItemDto() }?.toSet()
For non-null targets: items = this.items?.map { it.toItemDto() }?.toSet() ?: emptySet()
Why null becomes empty here but is an error elsewhere¶
A nullable source mapped to a non-null target is handled three ways in Kraft, depending on what the target is:
| Target | Nullable source, non-null target | Why |
|---|---|---|
| A collection or map | Falls back to an empty one (emptyList(), emptySet(), emptyMap(), emptyArray()…) |
An empty container is a natural "nothing here" value, and API DTOs often send null where the domain model wants an empty list. |
| A nested object | KRAFT-E014 |
Kraft never invents an object. There is no value it could build without your input. |
| A scalar through a converter | KRAFT-E005 |
A safe call on a converter yields Y?, and a scalar has no natural empty value. Declare an X? → Y converter, or supply the default with a whole-source @MapUsing (see custom converters). |
The collection fallback means a null source list and an empty one map to the same target value. If your domain treats them differently (for example, null means "not loaded" and empty means "loaded, nothing found"), keep the target nullable (List<ItemDto>?), so Kraft generates a plain safe call and the null survives. To turn null into something else, write a @MapUsing for that property.
Inside a map the rule is stricter: a nullable map value mapped to a non-null one is a type-mismatch error, because dropping the entry would also lose its key.
Converters on Elements¶
A registered @KraftConverter applies to collection elements exactly as it does to a single property, and it is called under its own name. With @KraftConverter fun Foo.intoBar(): Bar registered, List<Foo> to List<Bar> generates:
A converter registered for the non-null pair also serves nullable elements (it?.intoBar(), or mapNotNull when the target elements are non-null). As for a single property, an auto-detected nested mapper between two concrete classes takes precedence over a converter for the same pair. A hand-written @MapUsing that only repeats this element-wise call now triggers KRAFT-W001.
Nullable Elements¶
When the source element type is nullable but the target element type is not, Kraft uses mapNotNull instead of map to filter out null entries:
Generated: items = this.items.mapNotNull { it?.toItemDto() }
Kraft warns when a converter duplicates native mapping¶
You do not need a @MapUsing helper that only maps a collection element by element. If you write one anyway, for example fun List<RowDto>?.toRows(): List<Row> = this?.map { it.toRow() } ?: emptyList(), Kraft still uses it but reports warning KRAFT-W001. The warning shows the line Kraft would generate (here rows = this.rows?.map { it.toRow() } ?: emptyList()). Delete the helper, or keep it with @Suppress("KRAFT-W001") if it filters or sorts.
Maps¶
Map and MutableMap properties are mapped value by value, and their keys are mapped when the key types differ (see Map keys). Each value uses the operation Kraft would use for a single property of the value types: a nested mapper, an enum mapper, a @KraftConverter, or a value-class wrap or unwrap.
Generated: byId = this.byId.mapValues { it.value.toItemDto() }
| Source | Target | Generated |
|---|---|---|
Map<String, ItemSource> |
MutableMap<String, ItemDto> |
this.byId.mapValues { it.value.toItemDto() }.toMutableMap() |
Map<String, Int> |
MutableMap<String, Int> |
this.m.toMutableMap() |
Map<String, ItemSource>? |
Map<String, ItemDto> |
this.byId?.mapValues { it.value.toItemDto() } ?: emptyMap() |
Map<String, ItemSource>? |
MutableMap<String, ItemDto> |
this.byId?.mapValues { it.value.toItemDto() }?.toMutableMap() ?: mutableMapOf() |
Map<String, ItemSource?> |
Map<String, ItemDto?> |
this.byId.mapValues { it.value?.toItemDto() } |
A registered @KraftConverter fun Foo.intoBar(): Bar serves values the same way: m = this.m.mapValues { it.value.intoBar() }.
Two shapes are errors:
- Nullable values into non-null values (
Map<String, ItemSource?>intoMap<String, ItemDto>) fail withKRAFT-E005. Unlike a list, a map cannot drop the null: removing the value would also remove its key. - A map and a non-map (
Map<String, ItemSource>intoList<ItemDto>) fail withKRAFT-E005.
Map values and collection elements nest in each other to any depth: Map<String, List<ItemSource>> into Map<String, List<ItemDto>> generates groups = this.groups.mapValues { e0 -> e0.value.map { it.toItemDto() } }, and List<Map<String, ItemSource>> generates pages = this.pages.map { e0 -> e0.mapValues { it.value.toItemDto() } }. Each level follows its own rules, so a non-Set into a Set inside a map value is still KRAFT-E063. A hand-written @MapUsing that only repeats the mapValues call triggers KRAFT-W001.
Map keys¶
A map holds one entry per key, so a key mapping that turns two different keys into one would silently drop an entry. Kraft maps keys only through operations that are one-to-one:
- a value-class wrap (
Stringkey into aUserIdkey), - a value-class unwrap (
UserIdkey into aStringkey), - an enum mapping in which no two source entries map to the same target entry: an auto-derived enum mapping (same-named entries, always one-to-one) or a one-to-one
@MapEnum.
| Source | Target | Generated |
|---|---|---|
Map<String, ItemSource> |
Map<UserId, ItemDto> |
this.byUser.entries.associate { UserId(it.key) to it.value.toItemDto() } |
Map<UserId, Int> |
Map<String, Int> |
this.counts.mapKeys { it.key.value } |
Map<StatusDto, Int> |
Map<Status, Int> |
this.counts.mapKeys { it.key.toStatus() } |
mapKeys is used when only the keys change, mapValues when only the values change, and entries.associate { … } when both do. A MutableMap target adds .toMutableMap(), and a nullable source map is safe-called as for values.
Any other key operation fails with KRAFT-E064, because Kraft cannot prove it one-to-one:
- a nested mapper (
Map<KeySource, Int>intoMap<KeyDto, Int>), - a
@KraftConverter(@KraftConverter fun Long.toKey(): StringforMap<Long, Int>intoMap<String, Int>), - a
@MapEnumthat maps two or more source entries to the same target entry, - an enum mapper or converter compiled in another module, whose entry table Kraft cannot see.
A many-to-one @MapEnum is still fine for map values: Map<String, StatusDto> into Map<String, Status> generates this.m.mapValues { it.value.toStatus() }. Nullable keys are not mapped and report KRAFT-E005. To map keys some other way, map the property with @MapUsing.
Renamed Collection Properties¶
When the collection property has a different name on source and target, use FieldMapping to declare the rename. Auto-detection generates the child mapper:
data class TagSource(val value: String)
data class ArticleSource(val title: String, val articleTags: List<TagSource>)
data class TagDto(val value: String)
data class ArticleDto(val title: String, val tags: List<TagDto>)
@MapConfig(source = ArticleSource::class, target = ArticleDto::class)
@FieldMapping(source = "articleTags", target = "tags")
object ArticleMapper
Generated: tags = this.articleTags.map { it.toTagDto() }
Error Cases¶
- Element type is not a concrete class (e.g., an interface): compile-time error (
KRAFT-E015) naming the element type that "is not a concrete class" - Lossy collection conversion (e.g.,
Liston source,Seton target): fails withKRAFT-E063, see The Set rule - Map keys that could merge entries (a nested mapper, a
@KraftConverteror a many-to-one@MapEnumon the keys): fails withKRAFT-E064, see Map keys