Architecture¶
Pipeline Overview¶
@MapFrom / @MapTo / @MapConfig / @MapEnum (annotations)
|
v
+-----------+ +-----------------+ +---------------+
| Scanners | --> | DescriptorBuilder| --> | MapperGenerator|
+-----------+ +-----------------+ +---------------+
| | |
ClassAnnotation MapperDescriptor Generated .kt files
Scanner, Config (central IR) (extension functions)
ObjectScanner,
EnumMapScanner
The processing pipeline has three phases:
1. Scanning¶
Scanners read KSP symbols and produce raw scan results. Each scanner is responsible for a specific annotation family:
- ClassAnnotationScanner -- processes
@MapFromand@MapToannotations on data classes, producingClassMappingScanResultinstances that capture source/target types, field-level overrides, converters, and ignored properties. - ConfigObjectScanner -- processes
@MapConfigcompanion objects, producingConfigObjectScanResultinstances that capture bulk rename rules, shared converters, and field mappings declared outside the class itself. - EnumMapScanner -- processes
@MapEnumannotations, producingEnumMappingDescriptorinstances that capture enum constant correspondence.
2. Descriptor Building¶
DescriptorBuilder converts raw scan results into MapperDescriptor, the central intermediate representation. This phase:
- Resolves property mappings via the rule chain (see "Property Resolver Rule Chain" below).
- Validates type compatibility between source and target properties.
- Resolves implicit nested dependencies via DFS (see "Implicit Nested Dependency Resolution" below).
Concrete builders include ClassDescriptorBuilder, ConfigDescriptorBuilder, and ReverseDescriptorBuilder (for bidirectional mappings).
3. Code Generation¶
MapperGenerator implementations consume MapperDescriptor and emit Kotlin source files. The built-in generators produce extension functions (e.g., fun SourceDto.toTarget(): Target), but this phase is pluggable via the SPI (see "SPI: Custom Code Generators" below).
Module Structure¶
kraft-annotations (KMP: JVM, iOS, JS, WasmJs)
User-facing annotations (@MapFrom, @MapTo, @MapConfig, etc.)
No external dependencies.
kraft-core (JVM only)
depends on: kraft-annotations, KSP API
model/ -- MapperDescriptor, PropertyMappingStrategy, TypeInfo, PropertyInfo, MapperId
model/scan/ -- Raw scan results
scanner/ -- ClassAnnotationScanner, ConfigObjectScanner, EnumMapScanner
descriptor/ -- DescriptorBuilder, ClassDescriptorBuilder, ConfigDescriptorBuilder, ReverseDescriptorBuilder
descriptor/propertyresolver/ -- PropertyResolver + MappingRule chain
codegen/ -- MapperGenerator SPI, GenerationConfig, provider interfaces
util/ -- KraftKspConstants, AnnotationExtensions, LoggerExtensions
kraft-ksp (JVM only)
depends on: kraft-core, KotlinPoet
AutoMapperProcessor -- KSP SymbolProcessor entry point
AutoMapperProcessorProvider -- ServiceLoader registration
ExtensionMapperGenerator -- Built-in generator (extension functions via KotlinPoet)
EnumMapperGenerator -- Built-in enum mapper generator
CtorCallBuilder -- Builds constructor invocation CodeBlocks
TypeInfoExt -- Bridge: TypeInfo -> KotlinPoet ClassName
CodeGenUtils -- File naming, banner utilities
Key Types¶
MapperDescriptor¶
The central intermediate representation. Contains:
id: MapperId-- unique source/target qualified name pair.sourceType / targetType: TypeInfo-- source and target class info.propertyMappings: List<PropertyMappingStrategy>-- resolved strategy for each target property.nestedMappings: List<NestedMappingDescriptor>-- child mapper dependencies.converters: List<ConverterDescriptor>--@MapUsingconverter functions. Each carries aresolvedDirection(AUTO,FORWARD, orREVERSE) for directional filtering when@MapReverseis active.
PropertyMappingStrategy (sealed interface)¶
Six variants:
| Variant | Description | Example |
|---|---|---|
Direct |
Same name, same type | name = this.name |
Renamed |
Different name, same type | id = this.userId |
ConverterFunction |
Custom converter | label = Mapper.convert(this.count) |
NestedMapper |
Nested object | address = this.address.toAddressDto() |
Constant |
Literal value (reserved for future use) | -- |
Ignored |
Property skipped (must have default) | -- |
TypeInfo¶
Wraps a KSP type with metadata:
declaration: KSClassDeclaration-- KSP class declaration.ksType: KSType-- resolved type for equality checks.packageName / simpleName-- plain strings (no KotlinPoet dependency in kraft-core).qualifiedName-- computed:"$packageName.$simpleName".isNullable: Boolean.
MappingContext¶
Aggregated context passed to each MappingRule:
- Source properties, class-level renames, config-level renames.
- Converters, nested mappings, ignored properties.
- Logger for error reporting.
Property Resolver Rule Chain¶
PropertyResolver applies rules in order. The first match wins:
- IgnoreRule -- returns
Ignoredif the property is in the ignored set. - ConverterRule -- returns
ConverterFunctionif a@MapUsingtargets this property. - ClassOverrideRule -- returns
Renamedif@MapFieldprovides a counterpart name. - ConfigOverrideRule -- returns
Renamedif@FieldMappingprovides a source name. - NestedRule -- returns
NestedMapperif the property is a nested object/collection with a different mappable type. - DirectMatchRule -- returns
Directif a same-named, same-typed source property exists. - RequiredFieldErrorRule -- emits a compile-time error (the property has no default and no rule resolved it).
SPI: Custom Code Generators¶
The code generation phase is pluggable via a ServiceLoader-based SPI. You can replace or supplement the built-in ExtensionMapperGenerator with your own MapperGenerator that consumes MapperDescriptor and emits any output format — interface adapters, JSON schemas, documentation, test stubs, etc.
AutoMapperProcessor uses java.util.ServiceLoader to discover MapperGeneratorProvider implementations on the KSP classpath. If none are found, it falls back to the built-in generator. The same pattern applies to EnumMapperGeneratorProvider.
For a full step-by-step guide, data structure diagram, and working example, see Adding a Custom Code Generator.
Implicit Nested Dependency Resolution¶
When a MapperDescriptor references a nested type pair that does not have an explicit mapper, DescriptorBuilder resolves it via DFS:
- For each
NestedMapperstrategy, check if a descriptor already exists for the nested pair. - If not, synthesize a minimal descriptor using
ClassDescriptorBuilder. - Recurse into the synthesized descriptor's own nested dependencies.
- Circular dependencies are detected (gray/black DFS coloring) and reported as compile-time errors.