mapstruct
Companion project for the article MapStruct with Spring Boot 4 and Java Records: DTO Mapping Done Right on ankurm.com.
Every console block quoted in the article came out of output/. Transcripts 01-16 are written by the test suite
(so a claim that stops being true turns the build red); 17-24 are the compiler's own words, captured by
scripts/compile-errors.sh from the deliberately broken sources in src/broken/; 25 is read out of the
processor jar by scripts/capture-facts.sh.
There is deliberately no docs/ folder: the deeper material lives in collapsible "going deeper"
sections inside the article itself.
Versions
| Spring Boot | 4.1.2 (newest GA in Maven Central's metadata when this was written; the sibling modules use 4.1.1) |
| Spring Framework | 7.0.10 |
| MapStruct | 1.6.3 (newest GA; 1.7.0.Beta2 exists and is not used) |
| Jackson | 3.1.7 (tools.jackson) |
| Dozer (test scope, timing only) | 7.0.0 |
| JDK | 25 |
| Maven | 3.9 |
No Lombok. Records make it unnecessary here, and scripts/compile-errors.sh shows what Lombok does to the
processor path when you do use it (transcript 24).
Quickstart
export JAVA_HOME=/path/to/jdk-25
mvn test # runs the scenarios and rewrites output/01-16
./scripts/run-all.sh # everything, including 17-25 (needs the jars from one `mvn test` in ~/.m2)
The compiler plugin gets the generator through annotationProcessorPaths (see pom.xml). The generated
sources land in target/generated-sources/annotations/, where the tests read them back to print them.
Source layout
| Path | What it holds |
|---|---|
domain/ |
mutable entity-style classes: Customer, Address, Order, OrderLine |
dto/ |
records: CustomerDto, CustomerRequest, CustomerPatch, AddressDto, OrderDto, ReceiptDto (with a builder), ... |
mapper/ |
CentralConfig (Spring beans, constructor injection, unmapped target = error) and the mappers |
web/ |
a small controller and an in-memory repository, so the records can be seen over HTTP |
src/test/.../probe/ |
throwaway mappers that answer "what happens if...": hook order, @ObjectFactory, cycles, timing |
src/broken/ |
sources that must not compile; never part of the Maven build |
Captured output
| File | What it shows |
|---|---|
01-records-as-source-and-target.txt |
a record target has no setters; the generated code calls the constructor |
02-generated-implementation.txt |
the generated AddressMapperImpl and CustomerMapperImpl.toDto, read from target/ |
03-entity-to-record.txt |
nested objects, List/Set/Map, expression, constant, defaultValue, qualifiedByName, @Context |
04-record-to-entity.txt |
a record as source, @AfterMapping, @InheritInverseConfiguration, nullValueCheckStrategy |
05-null-strategies.txt |
IGNORE versus the default on a patch; RETURN_DEFAULT for whole objects and collections |
06-update-in-place.txt |
@MappingTarget updates mutate nested objects and collections, including immutable ones |
07-hooks-on-records.txt |
@BeforeMapping / @AfterMapping call order for a record target and a bean target |
08-object-factory-trap.txt |
an @ObjectFactory that returns a record: nothing is copied, no error |
09-builder-and-decorator.txt |
post-processing a record through its builder, and through @DecoratedWith |
10-context-and-cycles.txt |
@Context cycle tracking works for beans and cannot work for records |
11-testing-mappers-plain.txt |
Mappers.getMapper versus constructing the generated class |
12-testing-mappers-contextconfiguration.txt |
a Spring context of just the generated classes |
13-testing-mappers-springboottest.txt |
the same mapper inside @SpringBootTest |
14-property-level-test-strategy.txt |
one mutation per record component, and a completeness check |
15-spring-boot-4-records-over-http.txt |
MockMvc, Jackson 3, records in and out |
16-indicative-timing-vs-handwritten-and-dozer.txt |
indicative timings; differs on every run |
17-error-unmapped-target.txt |
javac: Unmapped target property |
18-error-ambiguous-mapping-methods.txt |
javac: Ambiguous mapping methods found |
19-error-missing-source-property.txt |
javac: No property named ... Did you mean ...? |
20-update-method-on-a-record-compiles-to-a-no-op.txt |
@MappingTarget on a record compiles, and generates an empty method |
21-error-cannot-map-type.txt |
javac: Can't map property |
22-error-decorator-constructor.txt |
javac: the generated decorator subclass has no constructor to call |
23-processor-on-the-plain-classpath.txt |
JDK 25 compiles without running the processor if it is only on -cp |
24-lombok-processor-order.txt |
Lombok before/after MapStruct, with and without lombok-mapstruct-binding |
25-processor-jar-facts.txt |
the processor service entry and its -A options, read with javap |