An OpenRewrite recipe that inlines MapStruct mapper implementations into plain Java code, giving you full ownership of your mapping logic.
New to OpenRewrite? OpenRewrite is an automated refactoring engine for Java (and other languages). It works by parsing your source files into a lossless syntax tree, applying recipes that modify that tree, and writing the changes back — preserving formatting and comments. Before diving into this README, skim the OpenRewrite introduction and the plugin setup for Gradle or Maven. The recipe catalog is also worth bookmarking.
MapStruct is terrible. It's greatest feature is its greatest flaw: it auto-maps your objects by matching field names. When it can't match, you either get a silently unmapped field (null in production) or you write the mapping logic as annotation attributes.
The result is a codebase where mappings are invisible, bugs are silent, and understanding what actually happens requires
reading both the interface and the generated implementation — a file that lives in build/, is never committed, and
disappears on a clean build. That is not easy to maintain; it is obscurity. MapStruct optimises for writing less code,
not for reading or maintaining it. Simple field-matching is manageable; but once a mapping requires logic, you are writing Java expressions inside
annotation strings, referencing methods by name in a context the LSP cannot reach. You need too many go-to-definition
into generated code to understand mapping logic because the code lacks cohesion.
This recipe removes MapStruct from your project by replacing every @Mapper interface with its generated
implementation, plus a series of mechanical refactorings to make the generated code better readable. From then the code
will be yours to read and improve.
"Software should be designed for ease of reading, not ease of writing." — John Ousterhout, A Philosophy of Software Design
Gradle — add to your root build.gradle:
plugins {
id "org.openrewrite.rewrite" version "latest.release"
}Find the latest version on the Gradle Plugin Portal.
Maven — add to your root pom.xml:
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>LATEST</version>
</plugin>Find the latest version on Maven Central.
The OpenRewrite plugin deliberately excludes build/ (or target/) from scanning. MapStruct writes its *Impl
classes there by default, so you must redirect annotation processor output into src/ first.
Gradle (build.gradle):
tasks.withType(JavaCompile).configureEach {
options.generatedSourceOutputDirectory = file("$projectDir/src/generated/java")
}
sourceSets {
main {
java.srcDirs += "$projectDir/src/generated/java"
}
}In a multi-module project, wrap the above in
subprojects { }in the rootbuild.gradle.
Maven (pom.xml):
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<generatedSourcesDirectory>${project.basedir}/src/generated/java</generatedSourcesDirectory>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-generated-sources</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.basedir}/src/generated/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>Run your formatter across the whole codebase before the recipe. This keeps the inlining diff clean: when you run the formatter again after the recipe, only the files the recipe touched will show up as changed — everything else is already formatted and stays untouched.
Gradle:
plugins {
id "com.diffplug.spotless" version "latest.release"
}
spotless {
java {
googleJavaFormat() // or palantirJavaFormat(), eclipse(), etc.
removeUnusedImports()
trimTrailingWhitespace()
endWithNewline()
}
}Run with ./gradlew spotlessApply.
See Spotless Gradle docs.
Maven:
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>LATEST</version>
<configuration>
<java>
<googleJavaFormat/>
<removeUnusedImports/>
<trimTrailingWhitespace/>
<endWithNewline/>
</java>
</configuration>
</plugin>Run with ./mvnw spotless:apply.
See Spotless Maven docs.
Use io.github.santunioni.recipes.PurgeMapstruct. It inlines every @Mapper into plain Java and applies cleanup (
unused imports, redundant parens, lambda simplification, formatting) — but only to the files it changes, keeping the
diff small.
Gradle (build.gradle):
dependencies {
rewrite "io.github.santunioni:purge-mapstruct:latest.release"
}
rewrite {
activeRecipe("io.github.santunioni.recipes.PurgeMapstruct")
}Maven (pom.xml, inside the rewrite-maven-plugin configuration):
<configuration>
<activeRecipes>
<recipe>io.github.santunioni.recipes.PurgeMapstruct</recipe>
</activeRecipes>
</configuration>
<dependencies>
<dependency>
<groupId>io.github.santunioni</groupId>
<artifactId>purge-mapstruct</artifactId>
<version>LATEST</version>
</dependency>
</dependencies>Compile first so MapStruct generates the *Impl files, run the recipe, then verify:
# Gradle
./gradlew compileJava compileTestJava \
&& ./gradlew rewriteRun \
&& ./gradlew compileJava compileTestJava test
# Maven
./mvnw compile test-compile \
&& ./mvnw rewrite:run \
&& ./mvnw compile test-compile testNote on Mockito
@Spy: If your tests spy on mapper fields usingwhen(myMapper.someMethod(...)).thenReturn(...), those stubs will break after inlining because the mapper is now a concrete class. The recipe automatically rewrites them todoReturn(...).when(myMapper).someMethod(...), which is the correct pattern for concrete-class spies. Pro tip for the future: don't mock your own code in tests, use it.
After the recipe runs, apply the formatter again — now only the inlined mapper files will be reformatted:
./gradlew spotlessApply # or ./mvnw spotless:applyIn the TODO file I am registering usages of MapStruct that the recipe currently doesn't support. I intend to cover them in future releases. But in the meantime, you can use the recipe if it is possible for you to remove those complex usages from your codebase manually as a pre-work.
I iterated on this recipe until it successfully purged MapStruct from two large production codebases. But I don't know your patterns. Maybe you are doing something I didn't cover.
If the recipe fails, produces broken code, or leaves something behind — open an issue on GitHub with a minimal reproducer, and I'll look into it. You are also invited to contribute a PR, as long as we discuss the reasoning behind the change first in an issue.