Architecture¶
How to read this codebase¶
Kraft is a KSP processor with one mental model: annotations in, extension functions out, one intermediate representation in the middle. Everything the processor does is a step toward building a MapperDescriptor (the IR) or a step consuming one.
flowchart LR
A["<span style='color:#141120'>Your annotated classes<br/>@MapFrom · @MapTo<br/>@MapConfig · @MapEnum</span>"] --> S["<span style='color:#f4f0e6'>Scanners</span>"]
S --> D["<span style='color:#f4f0e6'>DescriptorBuilder</span>"]
D --> IR(["<span style='color:#141120'>MapperDescriptor<br/>(central IR)</span>"])
IR --> G["<span style='color:#141120'>MapperGenerator</span>"]
G --> OUT["<span style='color:#141120'>Generated .kt files<br/>(extension functions)</span>"]
classDef anno fill:#fec14e,stroke:#141120,color:#141120
classDef scan fill:#4f5f83,stroke:#141120,color:#ffffff
classDef ir fill:#fb9144,stroke:#141120,color:#141120
classDef gen fill:#f07189,stroke:#141120,color:#141120
classDef out fill:#f4f0e6,stroke:#141120,color:#141120
class A anno
class S scan
class D scan
class IR ir
class G gen
class OUT out
The same colors are used on every diagram in this guide: gold = user input, navy = scanning/analysis, orange = the IR, coral = code generation, cream = generated output, red = error paths.
Fifteen-minute orientation, in reading order:
- This page, top to bottom.
AutoMapperProcessor.kt(kraft-ksp) — the entry point; ~185 lines that call everything below in sequence.MapperDescriptor(kraft-core,model/descriptor/) — the IR every phase reads or writes.- One test in
kraft-ksp/src/jvmTest(e.g.basic/) — see how behavior is specified end to end.
Where to look when you have a task:
| I want to... | Start at |
|---|---|
| Change how an annotation is interpreted | scanner/ (kraft-core) |
| Change which source property maps to a target property | descriptor/propertyresolver/rules/ |
| Change the generated Kotlin code | ExtensionMapperGenerator / CtorCallBuilder (kraft-ksp) |
| Add a new annotation | CONTRIBUTING.md, "How to Add a New Annotation" |
| Improve an error message | util/LoggerExtensions.kt |
Module structure¶
flowchart BT
anno["<span style='color:#141120'>kraft-annotations<br/>KMP: JVM · iOS · JS · WasmJs</span>"]
core["<span style='color:#f4f0e6'>kraft-core<br/>JVM</span>"]
ksp["<span style='color:#141120'>kraft-ksp<br/>JVM</span>"]
sample["<span style='color:#141120'>composeApp<br/>(sample, not published)</span>"]
core -->|api| anno
ksp -->|implementation| core
sample -.->|ksp plugin| ksp
sample -.-> anno
classDef anno fill:#fec14e,stroke:#141120,color:#141120
classDef scan fill:#4f5f83,stroke:#141120,color:#ffffff
classDef gen fill:#f07189,stroke:#141120,color:#141120
classDef out fill:#f4f0e6,stroke:#141120,color:#141120
class anno anno
class core scan
class ksp gen
class sample out
- kraft-annotations — user-facing annotations (
@MapFrom,@MapTo,@MapConfig, ...). Multiplatform because it is the only module that ends up on the consumer's compile classpath. No dependencies. - kraft-core — everything up to and including the IR, plus the generator SPI. JVM-only (it depends on the KSP API).
- kraft-ksp — the KSP processor entry point and the built-in KotlinPoet-based generators. This is the artifact users add with
ksp(...).
| kraft-core package | Contents |
|---|---|
model/ |
MapperDescriptor, PropertyMappingStrategy, TypeInfo, PropertyInfo, MapperId |
model/scan/ |
Raw scan results |
scanner/ |
The six scanners (table below) |
sides/ |
SideRegistry, AliasTemplate, PackageGlob — side-alias support |
descriptor/ |
DescriptorBuilder, ClassDescriptorBuilder, ConfigDescriptorBuilder, ReverseDescriptorBuilder |
descriptor/propertyresolver/ |
PropertyResolver + the MappingRule chain |
codegen/ |
MapperGenerator SPI, GenerationConfig, provider interfaces |
util/ |
KraftKspConstants, AnnotationExtensions, LoggerExtensions |
| kraft-ksp file | Role |
|---|---|
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 |
Phase 1: Scanning¶
Scanners read KSP symbols and produce raw scan results — no resolution logic yet, just structured facts about what the user declared.
flowchart LR
K["<span style='color:#141120'>KSP symbols<br/>(resolver)</span>"] --> SC["<span style='color:#f4f0e6'>Six scanners</span>"]
SC --> R["<span style='color:#f4f0e6'>Scan results<br/>(model/scan/)</span>"]
R --> DB["<span style='color:#141120'>DescriptorBuilder</span>"]
classDef anno fill:#fec14e,stroke:#141120,color:#141120
classDef scan fill:#4f5f83,stroke:#141120,color:#ffffff
classDef ir fill:#fb9144,stroke:#141120,color:#141120
class K anno
class SC scan
class R scan
class DB ir
| Scanner | Reads | Produces |
|---|---|---|
ClassAnnotationScanner |
@MapFrom / @MapTo on data classes |
ClassMappingScanResult — source/target types, field overrides, converters, ignored properties |
ConfigObjectScanner |
@MapConfig companion objects |
ConfigObjectScanResult — bulk renames, shared converters, field mappings declared outside the class |
EnumMapScanner |
@MapEnum |
EnumMappingDescriptor — enum constant correspondence |
GlobalConverterScanner |
top-level @KraftConverter functions in the current module |
GlobalConverterRegistry indexed by (sourceType, targetType) |
ClasspathConverterScanner |
@KraftConverterDelegate extensions published by upstream Kraft modules |
classpath-scoped GlobalConverterRegistry |
AutoEnumMappingDeriver |
every parent mapping pair | synthesized EnumMappingDescriptors for same-module enum pairs with matching entry names (skips pairs already covered by @MapEnum or @KraftConverter) |
Phase 2: Descriptor building¶
DescriptorBuilder converts scan results into MapperDescriptor, the central IR. This phase resolves property mappings via the rule chain, validates type compatibility, and resolves implicit nested dependencies (both below). Concrete builders: ClassDescriptorBuilder, ConfigDescriptorBuilder, and ReverseDescriptorBuilder (for @MapReverse bidirectional mappings).
The property resolver rule chain¶
For every target property, PropertyResolver walks this chain top to bottom. The first rule that matches produces the property's PropertyMappingStrategy; a miss falls through to the next rule. This chain is the heart of the library — most mapping behavior questions are answered by "which rule claimed this property, and why".
flowchart TD
R1["<span style='color:#f4f0e6'>1 · ConverterRule<br/>@MapUsing targets this property</span>"]
R2["<span style='color:#f4f0e6'>2 · IgnoreRule<br/>@MapIgnore / @MapIgnoreField</span>"]
R3["<span style='color:#f4f0e6'>3 · NestedRule<br/>nested mappable object or collection</span>"]
R4["<span style='color:#f4f0e6'>4 · GlobalConverterRule<br/>@KraftConverter matches the type pair</span>"]
R5["<span style='color:#f4f0e6'>5 · ClassOverrideRule<br/>@MapField rename</span>"]
R6["<span style='color:#f4f0e6'>6 · ConfigOverrideRule<br/>@FieldMapping rename</span>"]
R7["<span style='color:#f4f0e6'>7 · DirectMatchRule<br/>same name, same type</span>"]
R8["<span style='color:#f4f0e6'>8 · RequiredFieldErrorRule<br/>compile-time error</span>"]
R1 -->|miss| R2 -->|miss| R3 -->|miss| R4 -->|miss| R5 -->|miss| R6 -->|miss| R7 -->|miss| R8
classDef scan fill:#4f5f83,stroke:#141120,color:#ffffff
classDef err fill:#ca3e3f,stroke:#141120,color:#ffffff
class R1,R2,R3,R4,R5,R6,R7 scan
class R8 err
Ordering notes worth knowing:
GlobalConverterRuledeliberately runs before the rename rules andDirectMatchRuleso it can claim mismatched-type pairs before those rules would emit a type-mismatch error (see its KDoc).RequiredFieldErrorRuleis terminal: reaching it means the property is required (non-nullable, no default) and nothing could resolve it, so compilation fails with a detailed message fromLoggerExtensions.
Rules live in kraft-core/.../descriptor/propertyresolver/rules/, one file each; the order is defined in PropertyResolver.kt.
Implicit nested dependency resolution¶
When a descriptor references a nested type pair with no explicit mapper, DescriptorBuilder synthesizes one, depth-first:
flowchart TD
O["<span style='color:#141120'>OrderDto → Order<br/>(explicit @MapFrom)</span>"]
A["<span style='color:#141120'>AddressDto → Address<br/>(synthesized)</span>"]
C["<span style='color:#141120'>CityDto → City<br/>(synthesized)</span>"]
O -->|"address: AddressDto"| A
A -->|"city: CityDto"| C
C -.->|"order: OrderDto → cycle = compile error"| O
classDef anno fill:#fec14e,stroke:#141120,color:#141120
classDef ir fill:#fb9144,stroke:#141120,color:#141120
classDef err fill:#ca3e3f,stroke:#141120,color:#ffffff
class O anno
class A,C ir
linkStyle 2 stroke:#ca3e3f,color:#ca3e3f
- For each
NestedMapperstrategy, check whether a descriptor already exists for the nested pair. - If not, synthesize a minimal descriptor with
ClassDescriptorBuilder. - Recurse into the synthesized descriptor's own nested dependencies.
- Cycles are detected with gray/black DFS coloring and reported as compile-time errors (red edge above).
Phase 3: Code generation¶
MapperGenerator implementations consume MapperDescriptor and emit Kotlin source files. The built-in ExtensionMapperGenerator produces extension functions (fun SourceDto.toTarget(): Target); the phase is pluggable via a ServiceLoader SPI:
flowchart TD
P["<span style='color:#f4f0e6'>AutoMapperProcessor</span>"] --> Q{"<span style='color:#f4f0e6'>ServiceLoader finds a<br/>MapperGeneratorProvider?</span>"}
Q -->|yes| CG["<span style='color:#141120'>Your custom generator</span>"]
Q -->|no| BG["<span style='color:#141120'>Built-in<br/>ExtensionMapperGenerator</span>"]
classDef scan fill:#4f5f83,stroke:#141120,color:#ffffff
classDef gen fill:#f07189,stroke:#141120,color:#141120
class P,Q scan
class CG,BG gen
The same pattern applies to EnumMapperGeneratorProvider. The SPI is marked @ExperimentalKraftApi (opt-in required; it may change in any release). For the step-by-step guide and a working example, see Adding a Custom Code Generator.
Key types¶
MapperDescriptor¶
The central intermediate representation. Contains:
id: MapperId— unique source/target qualified name pair.sourceType / targetType: TypeInfo— source and target class info.source: MappingSource— whether the mapping was declared by a class annotation (@MapFrom/@MapTo) or a@MapConfigobject.propertyMappings: List<PropertyMappingStrategy>— resolved strategy for each target property.nestedMappings: List<NestedMappingDescriptor>— child mapper dependencies.enumMappings: List<EnumMappingDescriptor>— enum constant correspondences for this pair.converters: List<ConverterDescriptor>—@MapUsingconverter functions. Each carries aresolvedDirection(AUTO,FORWARD, orREVERSE) for directional filtering when@MapReverseis active.aliasEmitMode: AliasEmitMode— controls whether and how the side-alias delegate is emitted; defaults toINHERIT.
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 expression. No built-in MappingRule produces it yet (no annotation maps to it), but the generators already handle the variant |
isActive = true |
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— delegates to KSP'sdeclaration.qualifiedName?.asString(), which correctly handles nested types; falls back to"$packageName.$simpleName"only for anonymous or local declarations.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.