Collection Mapping¶
Kraft supports nested mapping inside List<T> and Set<T> collections. 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()
)
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()
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() }
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,
fieldMappings = [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 stating the element type "is not a concrete class"
- Mismatched collection kinds (e.g.,
Liston source,Seton target with the same element type): produces a type mismatch compilation error