# mapstruct Companion project for the article **[MapStruct with Spring Boot 4 and Java Records: DTO Mapping Done Right](https://ankurm.com/mapstruct-spring-boot-4-java-records-dto-mapping/)** on **[ankurm.com](https://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 ```bash 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` |