diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000000..f6faee6938
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ groups:
+ github-actions:
+ patterns:
+ - "*"
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 1a15fdec4a..0c6333a8ff 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -26,20 +26,20 @@ jobs:
build-mode: autobuild
steps:
- name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Initialize CodeQL
- uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
+ uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Set up Java
if: matrix.language == 'java-kotlin'
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
with:
distribution: 'zulu'
java-version: '21'
cache: 'maven'
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
+ uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
with:
category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/java-ea.yml b/.github/workflows/java-ea.yml
index 2937606d81..04897b2156 100644
--- a/.github/workflows/java-ea.yml
+++ b/.github/workflows/java-ea.yml
@@ -14,9 +14,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout'
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: 'Set up JDK'
- uses: oracle-actions/setup-java@v1
+ uses: oracle-actions/setup-java@fff43251af9936a0e6a4d5d0946e14f1680e9b6b # v1.5.0
with:
website: jdk.java.net
release: EA
diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml
index 7e218ad351..865820f6ab 100644
--- a/.github/workflows/macos.yml
+++ b/.github/workflows/macos.yml
@@ -13,9 +13,9 @@ jobs:
name: 'Mac OS'
runs-on: macos-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: 'Set up JDK 21'
- uses: actions/setup-java@v4
+ uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
with:
distribution: 'zulu'
java-version: 21
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 168cb9c7f0..d1ebef4642 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -20,9 +20,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout'
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: 'Set up JDK'
- uses: actions/setup-java@v4
+ uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
with:
distribution: 'zulu'
java-version: ${{ matrix.java }}
@@ -33,7 +33,7 @@ jobs:
run: ./mvnw jacoco:report
- name: 'Upload coverage to Codecov'
if: matrix.java == 21 && github.repository == 'mapstruct/mapstruct'
- uses: codecov/codecov-action@v5
+ uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
@@ -49,16 +49,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout'
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: 'Set up JDK 21 for building everything'
- uses: actions/setup-java@v4
+ uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
with:
distribution: 'zulu'
java-version: 21
- name: 'Install Processor'
run: ./mvnw ${MAVEN_ARGS} -DskipTests install -pl processor -am
- name: 'Set up JDK ${{ matrix.java }} for running integration tests'
- uses: actions/setup-java@v4
+ uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
with:
distribution: 'zulu'
java-version: ${{ matrix.java }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index aafb79a534..58440dcebd 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -16,13 +16,14 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
+ issues: write
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- name: Setup Java
- uses: actions/setup-java@v4
+ uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
with:
java-version: 21
distribution: 'zulu'
@@ -69,7 +70,7 @@ jobs:
- name: JReleaser output
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: jreleaser-release
path: |
@@ -98,7 +99,7 @@ jobs:
contents: read
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: mapstruct/mapstruct.org
ref: main
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
index d8d4de4263..0c6ccc1a25 100644
--- a/.github/workflows/windows.yml
+++ b/.github/workflows/windows.yml
@@ -13,9 +13,9 @@ jobs:
name: 'Windows'
runs-on: windows-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: 'Set up JDK 21'
- uses: actions/setup-java@v4
+ uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
with:
distribution: 'zulu'
java-version: 21
diff --git a/.gitignore b/.gitignore
index a33f0014ee..3cfac4e9f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,7 @@ test-output
.DS_Store
checkstyle.cache
.flattened-pom.xml
+
+.claude/settings.local.json
+.claude/worktrees
+.mcp.json
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index 8dea6c227c..216df05897 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -1,3 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip
diff --git a/NEXT_RELEASE_CHANGELOG.md b/NEXT_RELEASE_CHANGELOG.md
index 8392b63c60..e0f4cd31f0 100644
--- a/NEXT_RELEASE_CHANGELOG.md
+++ b/NEXT_RELEASE_CHANGELOG.md
@@ -1,58 +1,10 @@
### Features
-* Add support for JSpecify nullness annotations (#1243) - MapStruct now detects `@NonNull`, `@Nullable`, `@NullMarked` and `@NullUnmarked` from `org.jspecify.annotations` to control null check generation:
- - Source `@NonNull` skips null checks; target `@NonNull` always adds them
- - `@NonNull` source parameters skip the method-level null guard
- - `@NullMarked` / `@NullUnmarked` scope is resolved by walking method → class → outer class → package
- - Compile error when mapping a potentially nullable source to a `@NonNull` constructor parameter without a `defaultValue`
- - Can be disabled with the `mapstruct.disableJSpecify` compiler option
-* Add `URI` to `String` built-in conversions (#4018)
-
### Enhancements
-* Support `SET_TO_NULL` for overloaded target methods, requiring a cast (#3949)
-* Use multi-catch in generated code (#4021)
-* Use diamond operator for `new` expressions in generated code (#4045)
-* Always use factory methods for `LinkedHashMap` and `LinkedHashSet` when targeting `SequencedSet` and `SequencedMap` (#3990)
-* Improve annotation processing performance by removing regex matching from `Type.describe()` (#3991)
-* Add support for generics in arrays (#4050)
-
### Bugs
-* Prevent mapper generation from a type with a generic super bound to a type with a generic extends bound (#3994)
-* Fix location for Javadoc when generating the distribution zip
-
### Documentation
-* Add `SECURITY.md` and `.github/INCIDENT_RESPONSE.md`
-
### Build
-* Test on JDK 25 and 26, drop integration test on JDK 11 (#4039)
-* Specify OpenJDK 21 for the Jitpack build (#4042)
-* Add CodeQL custom workflow and set build mode for `java-kotlin` to `none`
-* Use diamond operator in test code (#4048)
-* Upgrade integration tests to JUnit 5 (#4023)
-* Update Maven compiler plugin (#3972)
-* Upgrade Freemarker to 2.3.34
-* Update license plugin
-* Enforce import order via Checkstyle `CustomImportOrder` (#4024)
-* Enforce spaces inside parentheses for control flow statements via Checkstyle
-* Simplify `fail` in `assertCheckstyleRules`
-* Remove deprecated `Number` API usage from tests
-* Use `StandardCharsets.UTF_8` in tests
-* Remove obsolete override of AssertJ version in integration tests
-* Let GitHub determine whether or not the released version is the latest
-* Simplify parallel-capable registration in `ModifiableURLClassLoader` test util (#4052)
-* Improve class loading for tests by replacing `LauncherDiscoveryListener` with `CompilerLauncherInterceptor` (#4051)
-
-### Internal
-
-* Upgrade internal `Visitor6` usages to `Visitor8`
-* Refactor `TypeFactory.getTypeParameters` (#4020)
-* Simplify boolean logic in `ValueMappingMethod` by removing inversion (#4007)
-* Remove unnecessary `keySet()` invocation (#3989)
-* Remove unused methods in `Fields` (#4010)
-* Add missing self reference in `GeneratedTypeBuilder` (#4009)
-* Fix self check in `equals` of `Type` (#3995)
-* Remove reflection from `isDefaultMethod` (#4053)
diff --git a/build-config/src/main/resources/build-config/checkstyle.xml b/build-config/src/main/resources/build-config/checkstyle.xml
index fa83d1d709..b15e108c5c 100644
--- a/build-config/src/main/resources/build-config/checkstyle.xml
+++ b/build-config/src/main/resources/build-config/checkstyle.xml
@@ -149,7 +149,7 @@
-
+
diff --git a/copyright.txt b/copyright.txt
index 39a863e531..74f18d89b1 100644
--- a/copyright.txt
+++ b/copyright.txt
@@ -8,6 +8,7 @@ Andreas Gudian - https://github.com/agudian
Andrei Arlou - https://github.com/Captain1653
Andres Jose Sebastian Rincon Gonzalez - https://github.com/stianrincon
Arne Seime - https://github.com/seime
+Burak Yildirim - https://github.com/bydrim
Cause Chung - https://github.com/cuzfrog
Christian Bandowski - https://github.com/chris922
Chris DeLashmutt - https://github.com/cdelashmutt-pivotal
@@ -36,6 +37,7 @@ Ivo Smid - https://github.com/bedla
Jason Bodnar - https://github.com/Blackbaud-JasonBodnar
Jeroen van Wilgenburg - https://github.com/jvwilge
Jeff Smyth - https://github.com/smythie86
+jmwbRyDWLeNsvtzrihGoY - https://github.com/jmwbRyDWLeNsvtzrihGoY
João Paulo Bassinello - https://github.com/jpbassinello
Jonathan Kraska - https://github.com/jakraska
Joshua Spoerri - https://github.com/spoerri
@@ -57,6 +59,7 @@ Paul Strugnell - https://github.com/ps-powa
Pascal Grün - https://github.com/pascalgn
Pavel Makhov - https://github.com/streetturtle
Peter Larson - https://github.com/pjlarson
+Raimund Klein - https://github.com/Chessray
Remko Plantenga - https://github.com/sonata82
Remo Meier - https://github.com/remmeier
Roel Mangelschots - https://github.com/rmschots
@@ -69,8 +72,10 @@ Samuel Wright - https://github.com/samwright
Sebastian Haberey - https://github.com/sebastianhaberey
Sebastian Hasait - https://github.com/shasait
Sean Huang - https://github.com/seanjob
+seonwoojung - https://github.com/seonwooj0810
Sjaak Derksen - https://github.com/sjaakd
Stefan May - https://github.com/osthus-sm
+Takch02 - https://github.com/Takch02
Taras Mychaskiw - https://github.com/twentylemon
Thibault Duperron - https://github.com/Zomzog
Tomáš Poledný - https://github.com/Saljack
@@ -84,6 +89,7 @@ Vincent Alexander Beelte - https://github.com/grandmasterpixel
Winter Andreas - https://github.dev/wandi34
Xiu Hong Kooi - https://github.com/kooixh
Yang Tang - https://github.com/tangyang9464
+Yevhen Vasyliev - https://github.com/yvasyliev
Zegveld - https://github.com/Zegveld
znight1020 - https://github.com/znight1020
zral - https://github.com/zyberzebra
diff --git a/core/src/main/java/org/mapstruct/ClassAccessibility.java b/core/src/main/java/org/mapstruct/ClassAccessibility.java
new file mode 100644
index 0000000000..c533b1ea32
--- /dev/null
+++ b/core/src/main/java/org/mapstruct/ClassAccessibility.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct;
+
+/**
+ * Determines whether a generated Mapper implementation class will be declared {@code public} or not.
+ *
+ * @author Raimund Klein
+ *
+ * @since 1.7.0
+ */
+public enum ClassAccessibility {
+ /**
+ * The generated Mapper will have the same modifier ({@code public} or none) as the annotated class or interface.
+ */
+ DEFAULT,
+ /**
+ * The generated Mapper will be declared {@code public}.
+ */
+ PUBLIC,
+ /**
+ * The generated Mapper will have no visibility modifier ("package-private").
+ */
+ PACKAGE_PRIVATE
+}
diff --git a/core/src/main/java/org/mapstruct/Mapper.java b/core/src/main/java/org/mapstruct/Mapper.java
index 398dc1870a..c502dfe8a9 100644
--- a/core/src/main/java/org/mapstruct/Mapper.java
+++ b/core/src/main/java/org/mapstruct/Mapper.java
@@ -14,6 +14,7 @@
import org.mapstruct.control.MappingControl;
import org.mapstruct.factory.Mappers;
+import static org.mapstruct.ClassAccessibility.DEFAULT;
import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION;
import static org.mapstruct.SubclassExhaustiveStrategy.COMPILE_ERROR;
@@ -388,4 +389,13 @@ NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy() default
* @since 1.5
*/
boolean suppressTimestampInGenerated() default false;
+
+ /**
+ * Determines the {@link ClassAccessibility} ({@code public} or package-private) for the generated Mapper
+ * implementation. Default is to mirror the interface or abstract class annotated by this {@code Mapper}.
+ *
+ * @return The {@link ClassAccessibility} ({@code public} or package-private) for the generated Mapper
+ * implementation
+ */
+ ClassAccessibility accessibility() default DEFAULT;
}
diff --git a/core/src/main/java/org/mapstruct/MapperConfig.java b/core/src/main/java/org/mapstruct/MapperConfig.java
index 8631562a56..a81e3e8d4f 100644
--- a/core/src/main/java/org/mapstruct/MapperConfig.java
+++ b/core/src/main/java/org/mapstruct/MapperConfig.java
@@ -14,6 +14,7 @@
import org.mapstruct.control.MappingControl;
import org.mapstruct.factory.Mappers;
+import static org.mapstruct.ClassAccessibility.DEFAULT;
import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION;
import static org.mapstruct.SubclassExhaustiveStrategy.COMPILE_ERROR;
@@ -357,5 +358,14 @@ MappingInheritanceStrategy mappingInheritanceStrategy()
* @since 1.5
*/
boolean suppressTimestampInGenerated() default false;
+
+ /**
+ * Determines the {@link ClassAccessibility} ({@code public} or package-private) for the generated Mapper
+ * implementation. Default is to mirror the interface or abstract class annotated by {@code Mapper}.
+ *
+ * @return The {@link ClassAccessibility} ({@code public} or package-private) for the generated Mapper
+ * implementation
+ */
+ ClassAccessibility accessibility() default DEFAULT;
}
diff --git a/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc b/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc
index 4c8be52503..306169193d 100644
--- a/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc
+++ b/documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc
@@ -328,6 +328,11 @@ The existing safety guards (`defaultValue` / `defaultExpression`, primitive unbo
==== Method-level source parameter
When the source parameter of a mapping method is annotated `@NonNull` (directly or via a `@NullMarked` scope), MapStruct skips the method-level null guard, since the caller is contractually obliged to pass a non-null value.
+This rule applies uniformly to bean, iterable, map, and stream mapping methods.
+
+==== Method-level return type
+
+When the return type of a mapping method is `@NonNull` (directly or via a `@NullMarked` scope), MapStruct forces `NullValueMappingStrategy.RETURN_DEFAULT` semantics. The generated method returns a default-constructed target (bean methods), an empty collection (`Iterable` / array mappings), an empty map (`Map` mappings), or `Stream.empty()` (stream mappings) rather than `null`, so the return contract is never violated. This rule applies regardless of the explicit `NullValueMappingStrategy` setting.
==== Constructor parameter constraint
diff --git a/documentation/src/main/asciidoc/chapter-4-retrieving-a-mapper.asciidoc b/documentation/src/main/asciidoc/chapter-4-retrieving-a-mapper.asciidoc
index 35afc93a51..ec817e0cdf 100644
--- a/documentation/src/main/asciidoc/chapter-4-retrieving-a-mapper.asciidoc
+++ b/documentation/src/main/asciidoc/chapter-4-retrieving-a-mapper.asciidoc
@@ -130,4 +130,24 @@ In that case utilize the `InjectionStrategy#SETTER` strategy.
[TIP]
====
For abstract classes or decorators setter injection should be used.
-====
\ No newline at end of file
+====
+
+[[class-accessibility]]
+=== Class accessibility
+
+By default, the generated Mapper implementation will have the same accessibility (`public` or none) as the interface or abstract class carrying the `@Mapper` annotation. Especially in conjunction with a DI framework, this might not be desired. If you wish to reduce the visibility of the generated class, you can make use of the `accessibility` attribute like this:
+
+====
+[source, java, linenums]
+[subs="verbatim,attributes"]
+----
+@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, accessibility = ClassAccessibility.PACKAGE_PRIVATE)
+public interface CarMapper {
+
+ CarDto carToCarDto(Car car);
+}
+
+----
+====
+
+In this case, the generated Mapper will have no modifier. Possible values are `ClassAccessibility.DEFAULT` (the behavior described above), `ClassAccessibility.PUBLIC`, and `ClassAccessibility.PACKAGE_PRIVATE` (no modifier).
diff --git a/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc b/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc
index d95e6c7a62..10c41506de 100644
--- a/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc
+++ b/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc
@@ -109,6 +109,14 @@ public interface CarMapper {
* Between `java.time.LocalDateTime` from Java 8 Date-Time package and `java.time.LocalDate` from the same package.
+* Between `java.time.ZonedDateTime` from Java 8 Date-Time package and `java.time.LocalDateTime` from the same package.
+
+* Between `java.time.OffsetDateTime` from Java 8 Date-Time package and `java.time.LocalDateTime` from the same package.
+
+* Between `java.time.ZonedDateTime` from Java 8 Date-Time package and `java.time.Instant` from the same package.
+
+* Between `java.time.OffsetDateTime` from Java 8 Date-Time package and `java.time.Instant` from the same package.
+
* Between `java.time.ZonedDateTime` from Java 8 Date-Time package and `java.util.Calendar`.
* Between `java.sql.Date` and `java.util.Date`
diff --git a/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java b/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java
index 60784d206a..042d6ec1da 100644
--- a/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java
+++ b/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java
@@ -89,7 +89,7 @@ void jsr330Test() {
@ProcessorTest(baseDir = "lombokBuilderTest", processorTypes = {
ProcessorTest.ProcessorType.JAVAC
})
- @DisabledOnJre(versions = 27)
+ @DisabledOnJre(versions = { 27, 28 })
void lombokBuilderTest() {
}
@@ -98,7 +98,7 @@ void lombokBuilderTest() {
ProcessorTest.ProcessorType.JAVAC_WITH_PATHS
})
@EnabledForJreRange(min = JRE.JAVA_11)
- @DisabledOnJre(versions = 27)
+ @DisabledOnJre(versions = { 27, 28 })
void lombokModuleTest() {
}
diff --git a/parent/pom.xml b/parent/pom.xml
index 1f340ba2ab..b444c1847a 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -39,7 +39,7 @@
13.0.0
5.14.1
2.2.0
- 1.12.0
+ 1.24.0
1
3.27.7
@@ -879,7 +879,10 @@
ALWAYS
- true
+
+ ALWAYS
+ true
+
false
@@ -910,6 +913,9 @@
${maven.multiModuleProjectDirectory}/NEXT_RELEASE_CHANGELOG.md
+
+ \d+\.\d+\.\d+\.(Alpha|Beta|M|RC)\d*
+
<#assign needVarDefine = (beforeMappingReferencesWithMappingTarget?has_content || afterMappingReferences?has_content) && !existingInstanceMapping />
diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/ValueMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/ValueMappingMethod.ftl
index 104fd98437..224b6c3dfa 100644
--- a/processor/src/main/resources/org/mapstruct/ap/internal/model/ValueMappingMethod.ftl
+++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/ValueMappingMethod.ftl
@@ -21,15 +21,35 @@
<#if nullTarget.targetAsException>throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} );<#else>return <@writeTarget target=nullTarget.target/>;#if>
}
- <@includeModel object=resultType/> ${resultName};
+ <#if versionInformation.isSourceVersionAtLeast14()>
+ <#if valueMappings.empty>
+ <#if defaultTarget.targetAsException>
+ <@includeModel object=resultType/> ${resultName};
+ throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} );
+ <#else>
+ <@includeModel object=resultType/> ${resultName} = <@writeTarget target=defaultTarget.target/>;
+ #if>
+ <#else>
+ <@includeModel object=resultType/> ${resultName} = switch ( ${sourceParameter.name} ) {
+ <#list valueMappings as valueMapping>
+ case <@writeSource source=valueMapping.source/> -> <#if valueMapping.targetAsException > throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} );<#else><@writeTarget target=valueMapping.target/>;#if>
+ #list>
+ <#if isDefaultTargetRequired()>
+ default -> <#if defaultTarget.targetAsException>throw new <@includeModel object=unexpectedValueMappingException/>( "Unexpected enum constant: " + ${sourceParameter.name} )<#else><@writeTarget target=defaultTarget.target/>#if>;
+ #if>
+ };
+ #if>
+ <#else>
+ <@includeModel object=resultType/> ${resultName};
- switch ( ${sourceParameter.name} ) {
- <#list valueMappings as valueMapping>
- case <@writeSource source=valueMapping.source/>: <#if valueMapping.targetAsException >throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} );<#else>${resultName} = <@writeTarget target=valueMapping.target/>;
- break;#if>
- #list>
- default: <#if defaultTarget.targetAsException >throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} )<#else>${resultName} = <@writeTarget target=defaultTarget.target/>#if>;
- }
+ switch ( ${sourceParameter.name} ) {
+ <#list valueMappings as valueMapping>
+ case <@writeSource source=valueMapping.source/>: <#if valueMapping.targetAsException >throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} );<#else>${resultName} = <@writeTarget target=valueMapping.target/>;
+ break;#if>
+ #list>
+ default: <#if defaultTarget.targetAsException >throw new <@includeModel object=unexpectedValueMappingException />( "Unexpected enum constant: " + ${sourceParameter.name} )<#else>${resultName} = <@writeTarget target=defaultTarget.target/>#if>;
+ }
+ #if>
<#list beforeMappingReferencesWithMappingTarget as callback>
<#if callback_index = 0>
diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Mapper.java
new file mode 100644
index 0000000000..ab053bc44e
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Mapper.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.bugs._3948;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper
+public interface Issue3948Mapper {
+
+ class ComparableField implements Comparable {
+
+ private final String code;
+
+ public ComparableField(String code) {
+ this.code = code;
+ }
+
+ @Override
+ public int compareTo(ComparableField o) {
+ return code.compareTo( o.code );
+ }
+ }
+
+ class Id implements Comparable> {
+ private final long value;
+
+ public Id(long value) {
+ this.value = value;
+ }
+
+ @Override
+ public int compareTo(Id o) {
+ return Long.compare( value, o.value );
+ }
+ }
+
+ class From {
+ private long id;
+ private String code;
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public void setCode(String code) {
+ this.code = code;
+ }
+ }
+
+ class To {
+ private Id id;
+ private ComparableField comparableField;
+
+ public Id getId() {
+ return id;
+ }
+
+ public void setId(Id id) {
+ this.id = id;
+ }
+
+ public ComparableField getComparableField() {
+ return comparableField;
+ }
+
+ public void setComparableField(ComparableField comparableField) {
+ this.comparableField = comparableField;
+ }
+ }
+
+ @Mapping(source = "code", target = "comparableField")
+ To convert(From from);
+
+ default Id longToId(Long id) {
+ return id == null ? null : new Id<>(id);
+ }
+
+ default ComparableField convert(String code) {
+ return code == null ? null : new ComparableField(code);
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Test.java
new file mode 100644
index 0000000000..8c80b2e802
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3948/Issue3948Test.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.bugs._3948;
+
+import org.junitpioneer.jupiter.Issue;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+
+/**
+ * @author hduelme
+ */
+@Issue("3948")
+public class Issue3948Test {
+
+ @ProcessorTest
+ @WithClasses(Issue3948Mapper.class)
+ public void shouldRequireExactGenericMatchForDeclaredSameTypes() {
+
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/ErroneousSetToDefaultMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/ErroneousSetToDefaultMapper.java
new file mode 100644
index 0000000000..4ede382781
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/ErroneousSetToDefaultMapper.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.bugs._4060;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+import org.mapstruct.BeanMapping;
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingTarget;
+import org.mapstruct.NullValuePropertyMappingStrategy;
+
+@Mapper
+public interface ErroneousSetToDefaultMapper {
+
+ class WithLocalDate {
+ private LocalDate value;
+
+ public LocalDate getValue() {
+ return value;
+ }
+
+ public void setValue(LocalDate value) {
+ this.value = value;
+ }
+ }
+
+ class WithBigDecimal {
+ private BigDecimal value;
+
+ public BigDecimal getValue() {
+ return value;
+ }
+
+ public void setValue(BigDecimal value) {
+ this.value = value;
+ }
+ }
+
+ class WithComparable {
+ private Comparable value;
+
+ public Comparable getValue() {
+ return value;
+ }
+
+ public void setValue(Comparable value) {
+ this.value = value;
+ }
+ }
+
+ abstract class AbstractValue {
+ }
+
+ class WithAbstractValue {
+ private AbstractValue value;
+
+ public AbstractValue getValue() {
+ return value;
+ }
+
+ public void setValue(AbstractValue value) {
+ this.value = value;
+ }
+ }
+
+ enum EnumValue {
+ FIRST
+ }
+
+ class WithEnumValue {
+ private EnumValue value;
+
+ public EnumValue getValue() {
+ return value;
+ }
+
+ public void setValue(EnumValue value) {
+ this.value = value;
+ }
+ }
+
+ @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT)
+ void updateLocalDate(@MappingTarget WithLocalDate target, WithLocalDate source);
+
+ @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT)
+ void updateBigDecimal(@MappingTarget WithBigDecimal target, WithBigDecimal source);
+
+ @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT)
+ void updateComparable(@MappingTarget WithComparable target, WithComparable source);
+
+ @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT)
+ void updateAbstractValue(@MappingTarget WithAbstractValue target, WithAbstractValue source);
+
+ @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT)
+ void updateEnumValue(@MappingTarget WithEnumValue target, WithEnumValue source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/Issue4060Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/Issue4060Test.java
new file mode 100644
index 0000000000..e0e51dac32
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/Issue4060Test.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.bugs._4060;
+
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult;
+import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic;
+import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@IssueKey("4060")
+public class Issue4060Test {
+
+ @ProcessorTest
+ @WithClasses(ErroneousSetToDefaultMapper.class)
+ @ExpectedCompilationOutcome(value = CompilationResult.FAILED,
+ diagnostics = {
+ @Diagnostic(type = ErroneousSetToDefaultMapper.class,
+ kind = javax.tools.Diagnostic.Kind.ERROR,
+ line = 87,
+ message = "LocalDate does not have an accessible parameterless constructor. " +
+ "Either change the nullValuePropertyMappingStrategy or define a defaultValue " +
+ "or a defaultExpression."),
+ @Diagnostic(type = ErroneousSetToDefaultMapper.class,
+ kind = javax.tools.Diagnostic.Kind.ERROR,
+ line = 90,
+ message = "BigDecimal does not have an accessible parameterless constructor. " +
+ "Either change the nullValuePropertyMappingStrategy or define a defaultValue " +
+ "or a defaultExpression."),
+ @Diagnostic(type = ErroneousSetToDefaultMapper.class,
+ kind = javax.tools.Diagnostic.Kind.ERROR,
+ line = 93,
+ message = "Comparable does not have an accessible parameterless constructor. " +
+ "Either change the nullValuePropertyMappingStrategy or define a defaultValue " +
+ "or a defaultExpression."),
+ @Diagnostic(type = ErroneousSetToDefaultMapper.class,
+ kind = javax.tools.Diagnostic.Kind.ERROR,
+ line = 96,
+ message = "ErroneousSetToDefaultMapper.AbstractValue does not have an accessible " +
+ "parameterless constructor. " +
+ "Either change the nullValuePropertyMappingStrategy or define a defaultValue " +
+ "or a defaultExpression."),
+ @Diagnostic(type = ErroneousSetToDefaultMapper.class,
+ kind = javax.tools.Diagnostic.Kind.ERROR,
+ line = 99,
+ message = "ErroneousSetToDefaultMapper.EnumValue does not have an accessible " +
+ "parameterless constructor. " +
+ "Either change the nullValuePropertyMappingStrategy or define a defaultValue " +
+ "or a defaultExpression.")
+ })
+ void setToDefaultWithoutParameterlessConstructorFails() {
+ }
+
+ @ProcessorTest
+ @WithClasses(SetToDefaultMapper.class)
+ void setToDefaultWithParameterlessConstructorResetsToNewInstance() {
+ SetToDefaultMapper.Target target = new SetToDefaultMapper.Target();
+ SetToDefaultMapper.Nested existing = new SetToDefaultMapper.Nested();
+ existing.setName( "existing" );
+ target.setNested( existing );
+ target.setText( "existing" );
+
+ SetToDefaultMapper.INSTANCE.update( target, new SetToDefaultMapper.Source() );
+
+ // source properties are null, SET_TO_DEFAULT resets them to a fresh default
+ assertThat( target.getNested() ).isNotNull();
+ assertThat( target.getNested() ).isNotSameAs( existing );
+ assertThat( target.getNested().getName() ).isNull();
+ assertThat( target.getText() ).isEmpty();
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/SetToDefaultMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/SetToDefaultMapper.java
new file mode 100644
index 0000000000..edfe7e9b3b
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_4060/SetToDefaultMapper.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.bugs._4060;
+
+import org.mapstruct.BeanMapping;
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingTarget;
+import org.mapstruct.NullValuePropertyMappingStrategy;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface SetToDefaultMapper {
+
+ SetToDefaultMapper INSTANCE = Mappers.getMapper( SetToDefaultMapper.class );
+
+ class Nested {
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+
+ class Target {
+ private Nested nested;
+ private String text;
+
+ public Nested getNested() {
+ return nested;
+ }
+
+ public void setNested(Nested nested) {
+ this.nested = nested;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+ }
+
+ class Source {
+ private Nested nested;
+ private String text;
+
+ public Nested getNested() {
+ return nested;
+ }
+
+ public void setNested(Nested nested) {
+ this.nested = nested;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+ }
+
+ @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT)
+ void update(@MappingTarget Target target, Source source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ClassAccessibilityTest.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ClassAccessibilityTest.java
new file mode 100644
index 0000000000..8bac5841f1
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ClassAccessibilityTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.classaccessibility;
+
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+
+import static java.lang.reflect.Modifier.isPrivate;
+import static java.lang.reflect.Modifier.isProtected;
+import static java.lang.reflect.Modifier.isPublic;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@WithClasses({
+ Source.class,
+ Target.class,
+ PublicAbstractionMapper.class,
+ PackageAbstractionMapper.class,
+ ForcedPublicMapper.class,
+ ForcedPackagePrivateMapper.class
+})
+public class ClassAccessibilityTest {
+
+ @ProcessorTest
+ public void shouldCreateModifierAccordingToAnnotation() throws Exception {
+ Class> publicLike = loadForMapper( PublicAbstractionMapper.class );
+ assertThat( isPublic( publicLike.getModifiers() ) ).isTrue();
+
+ Class> packageLike = loadForMapper( PackageAbstractionMapper.class );
+ assertThat( isDefault( packageLike.getModifiers() ) ).isTrue();
+
+ Class> forcedPublic = loadForMapper( ForcedPublicMapper.class );
+ assertThat( isPublic( forcedPublic.getModifiers() ) ).isTrue();
+
+ Class> forcedDefault = loadForMapper( ForcedPackagePrivateMapper.class );
+ assertThat( isDefault( forcedDefault.getModifiers() ) ).isTrue();
+ }
+
+ private static Class> loadForMapper(Class> mapper) throws ClassNotFoundException {
+ return Thread.currentThread().getContextClassLoader().loadClass( mapper.getName() + "Impl" );
+ }
+
+ private static boolean isDefault(int modifiers) {
+ return !isPublic( modifiers ) && !isProtected( modifiers ) && !isPrivate( modifiers );
+ }
+}
+
diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPackagePrivateMapper.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPackagePrivateMapper.java
new file mode 100644
index 0000000000..191aadb2a0
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPackagePrivateMapper.java
@@ -0,0 +1,15 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.classaccessibility;
+
+import org.mapstruct.ClassAccessibility;
+import org.mapstruct.Mapper;
+
+@Mapper(accessibility = ClassAccessibility.PACKAGE_PRIVATE)
+public interface ForcedPackagePrivateMapper {
+ Target map(Source value);
+}
+
diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPublicMapper.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPublicMapper.java
new file mode 100644
index 0000000000..fd6ced910a
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/ForcedPublicMapper.java
@@ -0,0 +1,15 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.classaccessibility;
+
+import org.mapstruct.ClassAccessibility;
+import org.mapstruct.Mapper;
+
+@Mapper(accessibility = ClassAccessibility.PUBLIC)
+interface ForcedPublicMapper {
+ Target map(Source value);
+}
+
diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PackageAbstractionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PackageAbstractionMapper.java
new file mode 100644
index 0000000000..d9f426b382
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PackageAbstractionMapper.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.classaccessibility;
+
+import org.mapstruct.Mapper;
+
+@Mapper
+interface PackageAbstractionMapper {
+ Target map(Source value);
+}
+
diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PublicAbstractionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PublicAbstractionMapper.java
new file mode 100644
index 0000000000..7c1b5de10f
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/PublicAbstractionMapper.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.classaccessibility;
+
+import org.mapstruct.Mapper;
+
+@Mapper
+public interface PublicAbstractionMapper {
+ Target map(Source value);
+}
+
diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Source.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Source.java
new file mode 100644
index 0000000000..feff965719
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Source.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.classaccessibility;
+
+/**
+ * @author Raimund Klein
+ **/
+public class Source {
+ private String foo;
+
+ public String getFoo() {
+ return foo;
+ }
+
+ public void setFoo(String foo) {
+ this.foo = foo;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Target.java b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Target.java
new file mode 100644
index 0000000000..e8c50a2531
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/classaccessibility/Target.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.classaccessibility;
+
+/**
+ * @author Raimund Klein
+ **/
+public class Target {
+ private String foo;
+
+ public String getFoo() {
+ return foo;
+ }
+
+ public void setFoo(String foo) {
+ this.foo = foo;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/ZonedOffsetDateTimeToLocalDateTimeConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/ZonedOffsetDateTimeToLocalDateTimeConversionTest.java
new file mode 100644
index 0000000000..ab17980e96
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/ZonedOffsetDateTimeToLocalDateTimeConversionTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.conversion.java8time;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion.Source;
+import org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion.SourceTargetMapper;
+import org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion.Target;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@WithClasses({ Source.class, Target.class, SourceTargetMapper.class })
+@IssueKey("4027")
+public class ZonedOffsetDateTimeToLocalDateTimeConversionTest {
+
+ @RegisterExtension
+ GeneratedSource generatedSource = new GeneratedSource()
+ .addComparisonToFixtureFor( SourceTargetMapper.class );
+
+ @ProcessorTest
+ public void testZonedDateTimeToLocalDateTimeMapping() {
+ ZonedDateTime zonedDateTime = ZonedDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneId.of( "UTC" ) );
+ Source source = new Source();
+ source.setZonedDateTime( zonedDateTime );
+ Target target = SourceTargetMapper.INSTANCE.toTarget( source );
+ assertThat( target.getZonedDateTime() )
+ .isEqualTo( LocalDateTime.of( 2024, 1, 1, 12, 30, 0 ) );
+ }
+
+ @ProcessorTest
+ public void testOffsetDateTimeToLocalDateTimeMapping() {
+ OffsetDateTime offsetDateTime = OffsetDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC );
+ Source source = new Source();
+ source.setOffsetDateTime( offsetDateTime );
+ Target target = SourceTargetMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOffsetDateTime() )
+ .isEqualTo( LocalDateTime.of( 2024, 1, 1, 12, 30, 0 ) );
+ }
+
+ @ProcessorTest
+ public void testLocalDateTimeToZonedDateTimeMapping() {
+ LocalDateTime localDateTime = LocalDateTime.of( 2024, 1, 1, 12, 30, 0 );
+ Target target = new Target();
+ target.setZonedDateTime( localDateTime );
+ Source source = SourceTargetMapper.INSTANCE.toSource( target );
+ assertThat( source.getZonedDateTime() )
+ .isEqualTo( ZonedDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ) );
+ }
+
+ @ProcessorTest
+ public void testLocalDateTimeToOffsetDateTimeMapping() {
+ LocalDateTime localDateTime = LocalDateTime.of( 2024, 1, 1, 12, 30, 0 );
+ Target target = new Target();
+ target.setOffsetDateTime( localDateTime );
+ Source source = SourceTargetMapper.INSTANCE.toSource( target );
+ assertThat( source.getOffsetDateTime() )
+ .isEqualTo( OffsetDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ) );
+ }
+
+ @ProcessorTest
+ public void testZonedDateTimeToInstantMapping() {
+ ZonedDateTime zonedDateTime = ZonedDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC );
+ Source source = new Source();
+ source.setZonedDateTimeAsInstant( zonedDateTime );
+ Target target = SourceTargetMapper.INSTANCE.toTarget( source );
+ assertThat( target.getZonedDateTimeAsInstant() )
+ .isEqualTo( Instant.parse( "2024-01-01T12:30:00Z" ) );
+ }
+
+ @ProcessorTest
+ public void testOffsetDateTimeToInstantMapping() {
+ OffsetDateTime offsetDateTime = OffsetDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC );
+ Source source = new Source();
+ source.setOffsetDateTimeAsInstant( offsetDateTime );
+ Target target = SourceTargetMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOffsetDateTimeAsInstant() )
+ .isEqualTo( Instant.parse( "2024-01-01T12:30:00Z" ) );
+ }
+
+ @ProcessorTest
+ public void testInstantToZonedDateTimeMapping() {
+ Instant instant = Instant.parse( "2024-01-01T12:30:00Z" );
+ Target target = new Target();
+ target.setZonedDateTimeAsInstant( instant );
+ Source source = SourceTargetMapper.INSTANCE.toSource( target );
+ assertThat( source.getZonedDateTimeAsInstant() )
+ .isEqualTo( ZonedDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ) );
+ }
+
+ @ProcessorTest
+ public void testInstantToOffsetDateTimeMapping() {
+ Instant instant = Instant.parse( "2024-01-01T12:30:00Z" );
+ Target target = new Target();
+ target.setOffsetDateTimeAsInstant( instant );
+ Source source = SourceTargetMapper.INSTANCE.toSource( target );
+ assertThat( source.getOffsetDateTimeAsInstant() )
+ .isEqualTo( OffsetDateTime.of( 2024, 1, 1, 12, 30, 0, 0, ZoneOffset.UTC ) );
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Source.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Source.java
new file mode 100644
index 0000000000..18bd99825c
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Source.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion;
+
+import java.time.OffsetDateTime;
+import java.time.ZonedDateTime;
+
+public class Source {
+ private ZonedDateTime zonedDateTime;
+ private OffsetDateTime offsetDateTime;
+ private ZonedDateTime zonedDateTimeAsInstant;
+ private OffsetDateTime offsetDateTimeAsInstant;
+
+ public ZonedDateTime getZonedDateTime() {
+ return zonedDateTime;
+ }
+
+ public void setZonedDateTime(ZonedDateTime zonedDateTime) {
+ this.zonedDateTime = zonedDateTime;
+ }
+
+ public OffsetDateTime getOffsetDateTime() {
+ return offsetDateTime;
+ }
+
+ public void setOffsetDateTime(OffsetDateTime offsetDateTime) {
+ this.offsetDateTime = offsetDateTime;
+ }
+
+ public ZonedDateTime getZonedDateTimeAsInstant() {
+ return zonedDateTimeAsInstant;
+ }
+
+ public void setZonedDateTimeAsInstant(ZonedDateTime zonedDateTimeAsInstant) {
+ this.zonedDateTimeAsInstant = zonedDateTimeAsInstant;
+ }
+
+ public OffsetDateTime getOffsetDateTimeAsInstant() {
+ return offsetDateTimeAsInstant;
+ }
+
+ public void setOffsetDateTimeAsInstant(OffsetDateTime offsetDateTimeAsInstant) {
+ this.offsetDateTimeAsInstant = offsetDateTimeAsInstant;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapper.java
new file mode 100644
index 0000000000..43bee5f77f
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapper.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface SourceTargetMapper {
+ SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
+
+ Target toTarget(Source source);
+
+ Source toSource(Target target);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Target.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Target.java
new file mode 100644
index 0000000000..b43b9f6843
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/Target.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+
+public class Target {
+ private LocalDateTime zonedDateTime;
+ private LocalDateTime offsetDateTime;
+ private Instant zonedDateTimeAsInstant;
+ private Instant offsetDateTimeAsInstant;
+
+ public LocalDateTime getOffsetDateTime() {
+ return offsetDateTime;
+ }
+
+ public void setOffsetDateTime(LocalDateTime offsetDateTime) {
+ this.offsetDateTime = offsetDateTime;
+ }
+
+ public LocalDateTime getZonedDateTime() {
+ return zonedDateTime;
+ }
+
+ public void setZonedDateTime(LocalDateTime zonedDateTime) {
+ this.zonedDateTime = zonedDateTime;
+ }
+
+ public Instant getZonedDateTimeAsInstant() {
+ return zonedDateTimeAsInstant;
+ }
+
+ public void setZonedDateTimeAsInstant(Instant zonedDateTimeAsInstant) {
+ this.zonedDateTimeAsInstant = zonedDateTimeAsInstant;
+ }
+
+ public Instant getOffsetDateTimeAsInstant() {
+ return offsetDateTimeAsInstant;
+ }
+
+ public void setOffsetDateTimeAsInstant(Instant offsetDateTimeAsInstant) {
+ this.offsetDateTimeAsInstant = offsetDateTimeAsInstant;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/conversion/url/URLConversionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conversion/url/URLConversionTest.java
index 1184134cf3..f5a3b7f7e8 100644
--- a/processor/src/test/java/org/mapstruct/ap/test/conversion/url/URLConversionTest.java
+++ b/processor/src/test/java/org/mapstruct/ap/test/conversion/url/URLConversionTest.java
@@ -6,7 +6,7 @@
package org.mapstruct.ap.test.conversion.url;
import java.net.MalformedURLException;
-import java.net.URL;
+import java.net.URI;
import org.mapstruct.ap.testutil.ProcessorTest;
import org.mapstruct.ap.testutil.WithClasses;
@@ -25,7 +25,7 @@ public class URLConversionTest {
@ProcessorTest
public void shouldApplyURLConversion() throws MalformedURLException {
Source source = new Source();
- source.setURL( new URL("https://mapstruct.org/") );
+ source.setURL( URI.create( "https://mapstruct.org/" ).toURL() );
Target target = URLMapper.INSTANCE.sourceToTarget( source );
@@ -41,13 +41,13 @@ public void shouldApplyReverseURLConversion() throws MalformedURLException {
Source source = URLMapper.INSTANCE.targetToSource( target );
assertThat( source ).isNotNull();
- assertThat( source.getURL() ).isEqualTo( new URL( target.getURL() ) );
+ assertThat( source.getURL() ).isEqualTo( URI.create( target.getURL() ).toURL() );
}
@ProcessorTest
public void shouldHandleInvalidURLString() {
Target target = new Target();
- target.setInvalidURL( "XXXXXXXXX" );
+ target.setInvalidURL( "xxxxxx://mapstruct.org/" );
assertThatThrownBy( () -> URLMapper.INSTANCE.targetToSource( target ) )
.isInstanceOf( RuntimeException.class )
@@ -57,7 +57,7 @@ public void shouldHandleInvalidURLString() {
@ProcessorTest
public void shouldHandleInvalidURLStringWithMalformedURLException() {
Target target = new Target();
- target.setInvalidURL( "XXXXXXXXX" );
+ target.setInvalidURL( "xxxxxx://mapstruct.org/" );
assertThatThrownBy( () -> URLMapper.INSTANCE.targetToSourceWithMalformedURLException( target ) )
.isInstanceOf( MalformedURLException.class );
diff --git a/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java b/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java
index f460180651..57c2099b8b 100644
--- a/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java
+++ b/processor/src/test/java/org/mapstruct/ap/test/gem/EnumGemsTest.java
@@ -10,6 +10,7 @@
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
+import org.mapstruct.ClassAccessibility;
import org.mapstruct.CollectionMappingStrategy;
import org.mapstruct.ConditionStrategy;
import org.mapstruct.InjectionStrategy;
@@ -17,6 +18,7 @@
import org.mapstruct.NullValueCheckStrategy;
import org.mapstruct.NullValueMappingStrategy;
import org.mapstruct.ReportingPolicy;
+import org.mapstruct.ap.internal.gem.ClassAccessibilityGem;
import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem;
import org.mapstruct.ap.internal.gem.ConditionStrategyGem;
import org.mapstruct.ap.internal.gem.InjectionStrategyGem;
@@ -75,6 +77,12 @@ public void conditionStrategyGemIsCorrect() {
namesOf( ConditionStrategyGem.values() ) );
}
+ @Test
+ public void classAccesibilityGemIsCorrect() {
+ assertThat( namesOf( ClassAccessibility.values() ) ).isEqualTo(
+ namesOf( ClassAccessibilityGem.values() ) );
+ }
+
private static List namesOf(Enum>[] values) {
return Stream.of( values )
.map( Enum::name )
diff --git a/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/CentralConfig.java b/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/CentralConfig.java
index 81dc9a452a..3caeb8cbba 100644
--- a/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/CentralConfig.java
+++ b/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/CentralConfig.java
@@ -5,6 +5,7 @@
*/
package org.mapstruct.ap.test.mapperconfig;
+import org.mapstruct.ClassAccessibility;
import org.mapstruct.MapperConfig;
import org.mapstruct.ReportingPolicy;
@@ -12,7 +13,9 @@
*
* @author Sjaak Derksen
*/
-@MapperConfig(uses = { CustomMapperViaMapperConfig.class }, unmappedTargetPolicy = ReportingPolicy.ERROR )
+@MapperConfig(uses = {CustomMapperViaMapperConfig.class},
+ unmappedTargetPolicy = ReportingPolicy.ERROR,
+ accessibility = ClassAccessibility.PACKAGE_PRIVATE)
public class CentralConfig {
}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/ConfigTest.java b/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/ConfigTest.java
index 1f7fe423f5..a13db4ab0b 100644
--- a/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/ConfigTest.java
+++ b/processor/src/test/java/org/mapstruct/ap/test/mapperconfig/ConfigTest.java
@@ -12,6 +12,9 @@
import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic;
import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome;
+import static java.lang.reflect.Modifier.isPrivate;
+import static java.lang.reflect.Modifier.isProtected;
+import static java.lang.reflect.Modifier.isPublic;
import static org.assertj.core.api.Assertions.assertThat;
/**
@@ -49,6 +52,13 @@ public void shouldUseCustomMapperViaMapperConfigForFooToDto() {
assertThat( source.getFoo().getCreatedBy() ).isEqualTo( CustomMapperViaMapperConfig.class.getSimpleName() );
}
+ @ProcessorTest
+ @WithClasses( { Target.class, SourceTargetMapper.class } )
+ public void shouldDeclareMapperImplementationAsPackagePrivate() throws ClassNotFoundException {
+ Class> implementation = loadForMapper( SourceTargetMapper.class );
+ assertThat( isDefault( implementation.getModifiers() ) ).isTrue();
+ }
+
@ProcessorTest
@WithClasses( { TargetNoFoo.class, SourceTargetMapperWarn.class } )
@ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED,
@@ -70,4 +80,12 @@ public void shouldUseWARNViaMapper() {
})
public void shouldUseERRORViaMapperConfig() {
}
+
+ private static Class> loadForMapper(Class> mapper) throws ClassNotFoundException {
+ return Thread.currentThread().getContextClassLoader().loadClass( mapper.getName() + "Impl" );
+ }
+
+ private static boolean isDefault(int modifiers) {
+ return !isPublic( modifiers ) && !isProtected( modifiers ) && !isPrivate( modifiers );
+ }
}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapper.java
new file mode 100644
index 0000000000..d705652832
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapper.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import org.jspecify.annotations.NullMarked;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@NullMarked
+@Mapper
+public interface JSpecifyCollectionPropertyMapper {
+
+ JSpecifyCollectionPropertyMapper INSTANCE = Mappers.getMapper( JSpecifyCollectionPropertyMapper.class );
+
+ NullMarkedCollectionTargetBean map(NullMarkedCollectionSourceBean source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyTest.java
new file mode 100644
index 0000000000..bf194fc08f
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.Arrays;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.WithJSpecify;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@IssueKey("1243")
+@WithJSpecify
+class JSpecifyCollectionPropertyTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource();
+
+ @ProcessorTest
+ @WithClasses({
+ NullMarkedCollectionSourceBean.class,
+ NullMarkedCollectionTargetBean.class,
+ JSpecifyCollectionPropertyMapper.class
+ })
+ void collectionPropertyWithNonNullSourceSkipsNullCheck() {
+ generatedSource.addComparisonToFixtureFor( JSpecifyCollectionPropertyMapper.class );
+
+ NullMarkedCollectionSourceBean source = new NullMarkedCollectionSourceBean();
+ source.setValues( Arrays.asList( "a", "b", "c" ) );
+
+ NullMarkedCollectionTargetBean target = JSpecifyCollectionPropertyMapper.INSTANCE.map( source );
+
+ assertThat( target ).isNotNull();
+ assertThat( target.getValues() ).containsExactly( "a", "b", "c" );
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledContainerSourceMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledContainerSourceMapper.java
new file mode 100644
index 0000000000..2de32c4235
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledContainerSourceMapper.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.List;
+
+import org.jspecify.annotations.NullMarked;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * Dedicated to {@link JSpecifyDisabledTest} so its generated implementation is not shared (and pre-loaded) by an
+ * enabled test in the same JVM. With JSpecify enabled the {@code @NonNull} source would skip the method-level guard.
+ */
+@NullMarked
+@Mapper
+public interface JSpecifyDisabledContainerSourceMapper {
+
+ JSpecifyDisabledContainerSourceMapper INSTANCE =
+ Mappers.getMapper( JSpecifyDisabledContainerSourceMapper.class );
+
+ List mapAll(List sources);
+
+ NullMarkedTargetBean map(NullMarkedSourceBean source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledNonNullReturnMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledNonNullReturnMapper.java
new file mode 100644
index 0000000000..bcdb0bc0ca
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledNonNullReturnMapper.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.List;
+
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * Dedicated to {@link JSpecifyDisabledTest} so its generated implementation is not shared (and pre-loaded) by an
+ * enabled test in the same JVM. With JSpecify enabled the {@code @NonNull} return would force RETURN_DEFAULT.
+ */
+@NullMarked
+@Mapper
+public interface JSpecifyDisabledNonNullReturnMapper {
+
+ JSpecifyDisabledNonNullReturnMapper INSTANCE =
+ Mappers.getMapper( JSpecifyDisabledNonNullReturnMapper.class );
+
+ List mapAll(@Nullable List sources);
+
+ NullMarkedTargetBean map(NullMarkedSourceBean source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledTest.java
index 547365467f..d669952737 100644
--- a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledTest.java
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyDisabledTest.java
@@ -54,4 +54,30 @@ public void disabledFlagSkipsConstructorNonNullHardError() {
// The JSpecify-driven hard error for a @Nullable source mapped to a @NonNull constructor
// parameter is purely a JSpecify signal; with the flag disabled it must not be raised.
}
+
+ @ProcessorTest
+ @WithClasses({
+ NullMarkedSourceBean.class,
+ NullMarkedTargetBean.class,
+ JSpecifyDisabledContainerSourceMapper.class
+ })
+ public void disabledFlagKeepsContainerMethodSourceGuard() {
+ // With JSpecify enabled the @NonNull source parameter skips the method-level null guard. When disabled,
+ // the resolver reports UNKNOWN, so the unconditional "if (sources == null) return null;" guard is kept and
+ // mapping a null source returns null rather than throwing an NPE.
+ assertThat( JSpecifyDisabledContainerSourceMapper.INSTANCE.mapAll( null ) ).isNull();
+ }
+
+ @ProcessorTest
+ @WithClasses({
+ NullMarkedSourceBean.class,
+ NullMarkedTargetBean.class,
+ JSpecifyDisabledNonNullReturnMapper.class
+ })
+ public void disabledFlagSkipsNonNullReturnForcing() {
+ // With JSpecify enabled the @NonNull return forces RETURN_DEFAULT (empty list for a null source). When
+ // disabled, that forcing is suppressed and the default RETURN_NULL strategy applies, so a null source
+ // maps to null.
+ assertThat( JSpecifyDisabledNonNullReturnMapper.INSTANCE.mapAll( null ) ).isNull();
+ }
}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapper.java
new file mode 100644
index 0000000000..5a1d0b0d68
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapper.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.List;
+
+import org.jspecify.annotations.NullMarked;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@NullMarked
+@Mapper
+public interface JSpecifyIterableMethodMapper {
+
+ JSpecifyIterableMethodMapper INSTANCE = Mappers.getMapper( JSpecifyIterableMethodMapper.class );
+
+ List mapAll(List sources);
+
+ NullMarkedTargetBean map(NullMarkedSourceBean source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodTest.java
new file mode 100644
index 0000000000..ceed2236f6
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.WithJSpecify;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@IssueKey("1243")
+@WithJSpecify
+class JSpecifyIterableMethodTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource();
+
+ @ProcessorTest
+ @WithClasses({
+ NullMarkedSourceBean.class,
+ NullMarkedTargetBean.class,
+ JSpecifyIterableMethodMapper.class
+ })
+ void iterableMethodWithNonNullSourceSkipsMethodGuard() {
+ generatedSource.addComparisonToFixtureFor( JSpecifyIterableMethodMapper.class );
+
+ NullMarkedSourceBean source = new NullMarkedSourceBean();
+ source.setNonNullByDefault( "value" );
+
+ List targets = JSpecifyIterableMethodMapper.INSTANCE.mapAll( Arrays.asList( source ) );
+
+ assertThat( targets ).hasSize( 1 );
+ assertThat( targets.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" );
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapper.java
new file mode 100644
index 0000000000..cdb844795b
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapper.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.Map;
+
+import org.jspecify.annotations.NullMarked;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@NullMarked
+@Mapper
+public interface JSpecifyMapMethodMapper {
+
+ JSpecifyMapMethodMapper INSTANCE = Mappers.getMapper( JSpecifyMapMethodMapper.class );
+
+ Map mapAll(Map sources);
+
+ NullMarkedTargetBean map(NullMarkedSourceBean source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodTest.java
new file mode 100644
index 0000000000..4673841873
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.WithJSpecify;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@IssueKey("1243")
+@WithJSpecify
+class JSpecifyMapMethodTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource();
+
+ @ProcessorTest
+ @WithClasses({
+ NullMarkedSourceBean.class,
+ NullMarkedTargetBean.class,
+ JSpecifyMapMethodMapper.class
+ })
+ void mapMethodWithNonNullSourceSkipsMethodGuard() {
+ generatedSource.addComparisonToFixtureFor( JSpecifyMapMethodMapper.class );
+
+ NullMarkedSourceBean source = new NullMarkedSourceBean();
+ source.setNonNullByDefault( "value" );
+
+ Map targets =
+ JSpecifyMapMethodMapper.INSTANCE.mapAll( Collections.singletonMap( "k", source ) );
+
+ assertThat( targets ).hasSize( 1 );
+ assertThat( targets.get( "k" ).getNonNullByDefault() ).isEqualTo( "value" );
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapper.java
new file mode 100644
index 0000000000..8401e56682
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapper.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+import org.mapstruct.BeanMapping;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.factory.Mappers;
+
+@NullMarked
+@Mapper
+public interface JSpecifyNonNullReturnBeanMapper {
+
+ JSpecifyNonNullReturnBeanMapper INSTANCE = Mappers.getMapper( JSpecifyNonNullReturnBeanMapper.class );
+
+ @BeanMapping(ignoreByDefault = true)
+ @Mapping(target = "nonNullByDefault", source = "value")
+ NullMarkedTargetBean map(@Nullable JSpecifyNonNullReturnBeanSourceBean source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanSourceBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanSourceBean.java
new file mode 100644
index 0000000000..d4bdb926c5
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanSourceBean.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import org.jspecify.annotations.NullMarked;
+
+@NullMarked
+public class JSpecifyNonNullReturnBeanSourceBean {
+
+ private String value;
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanTest.java
new file mode 100644
index 0000000000..ccac0e0653
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.WithJSpecify;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@IssueKey("1243")
+@WithJSpecify
+class JSpecifyNonNullReturnBeanTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource();
+
+ @ProcessorTest
+ @WithClasses({
+ JSpecifyNonNullReturnBeanSourceBean.class,
+ NullMarkedTargetBean.class,
+ JSpecifyNonNullReturnBeanMapper.class
+ })
+ void nonNullReturnForcesMapNullToDefault() {
+ generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnBeanMapper.class );
+
+ NullMarkedTargetBean fromNull = JSpecifyNonNullReturnBeanMapper.INSTANCE.map( null );
+
+ assertThat( fromNull ).isNotNull();
+ assertThat( fromNull.getNonNullByDefault() ).isNull();
+
+ JSpecifyNonNullReturnBeanSourceBean source = new JSpecifyNonNullReturnBeanSourceBean();
+ source.setValue( "value" );
+
+ NullMarkedTargetBean fromSource = JSpecifyNonNullReturnBeanMapper.INSTANCE.map( source );
+
+ assertThat( fromSource ).isNotNull();
+ assertThat( fromSource.getNonNullByDefault() ).isEqualTo( "value" );
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapper.java
new file mode 100644
index 0000000000..4f3fd1c0e2
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapper.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.List;
+
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@NullMarked
+@Mapper
+public interface JSpecifyNonNullReturnIterableMapper {
+
+ JSpecifyNonNullReturnIterableMapper INSTANCE = Mappers.getMapper( JSpecifyNonNullReturnIterableMapper.class );
+
+ List mapAll(@Nullable List sources);
+
+ NullMarkedTargetBean map(NullMarkedSourceBean source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableTest.java
new file mode 100644
index 0000000000..f4ba29bb92
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.WithJSpecify;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@IssueKey("1243")
+@WithJSpecify
+class JSpecifyNonNullReturnIterableTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource();
+
+ @ProcessorTest
+ @WithClasses({
+ NullMarkedSourceBean.class,
+ NullMarkedTargetBean.class,
+ JSpecifyNonNullReturnIterableMapper.class
+ })
+ void nonNullReturnIterableForcesEmptyDefault() {
+ generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnIterableMapper.class );
+
+ List fromNull = JSpecifyNonNullReturnIterableMapper.INSTANCE.mapAll( null );
+
+ assertThat( fromNull ).isEmpty();
+
+ NullMarkedSourceBean source = new NullMarkedSourceBean();
+ source.setNonNullByDefault( "value" );
+
+ List fromSource =
+ JSpecifyNonNullReturnIterableMapper.INSTANCE.mapAll( Arrays.asList( source ) );
+
+ assertThat( fromSource ).hasSize( 1 );
+ assertThat( fromSource.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" );
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapper.java
new file mode 100644
index 0000000000..8e6a0cb885
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapper.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.Map;
+
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@NullMarked
+@Mapper
+public interface JSpecifyNonNullReturnMapMapper {
+
+ JSpecifyNonNullReturnMapMapper INSTANCE = Mappers.getMapper( JSpecifyNonNullReturnMapMapper.class );
+
+ Map mapAll(@Nullable Map sources);
+
+ NullMarkedTargetBean map(NullMarkedSourceBean source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapTest.java
new file mode 100644
index 0000000000..b96d3f3fc0
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.WithJSpecify;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@IssueKey("1243")
+@WithJSpecify
+class JSpecifyNonNullReturnMapTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource();
+
+ @ProcessorTest
+ @WithClasses({
+ NullMarkedSourceBean.class,
+ NullMarkedTargetBean.class,
+ JSpecifyNonNullReturnMapMapper.class
+ })
+ void nonNullReturnMapForcesEmptyDefault() {
+ generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnMapMapper.class );
+
+ Map fromNull = JSpecifyNonNullReturnMapMapper.INSTANCE.mapAll( null );
+
+ assertThat( fromNull ).isEmpty();
+
+ NullMarkedSourceBean source = new NullMarkedSourceBean();
+ source.setNonNullByDefault( "value" );
+
+ Map fromSource =
+ JSpecifyNonNullReturnMapMapper.INSTANCE.mapAll( Collections.singletonMap( "k", source ) );
+
+ assertThat( fromSource ).hasSize( 1 );
+ assertThat( fromSource.get( "k" ).getNonNullByDefault() ).isEqualTo( "value" );
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapper.java
new file mode 100644
index 0000000000..62d061a001
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapper.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.stream.Stream;
+
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@NullMarked
+@Mapper
+public interface JSpecifyNonNullReturnStreamMapper {
+
+ JSpecifyNonNullReturnStreamMapper INSTANCE = Mappers.getMapper( JSpecifyNonNullReturnStreamMapper.class );
+
+ Stream mapAll(@Nullable Stream sources);
+
+ NullMarkedTargetBean map(NullMarkedSourceBean source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamTest.java
new file mode 100644
index 0000000000..fa62973ebf
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.WithJSpecify;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@IssueKey("1243")
+@WithJSpecify
+class JSpecifyNonNullReturnStreamTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource();
+
+ @ProcessorTest
+ @WithClasses({
+ NullMarkedSourceBean.class,
+ NullMarkedTargetBean.class,
+ JSpecifyNonNullReturnStreamMapper.class
+ })
+ void nonNullReturnStreamForcesEmptyDefault() {
+ generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnStreamMapper.class );
+
+ Stream fromNull = JSpecifyNonNullReturnStreamMapper.INSTANCE.mapAll( null );
+
+ assertThat( fromNull ).isNotNull();
+ assertThat( fromNull.collect( Collectors.toList() ) ).isEmpty();
+
+ NullMarkedSourceBean source = new NullMarkedSourceBean();
+ source.setNonNullByDefault( "value" );
+
+ List fromSource = JSpecifyNonNullReturnStreamMapper.INSTANCE
+ .mapAll( Stream.of( source ) )
+ .collect( Collectors.toList() );
+
+ assertThat( fromSource ).hasSize( 1 );
+ assertThat( fromSource.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" );
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapper.java
new file mode 100644
index 0000000000..b79763d712
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapper.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.List;
+
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingTarget;
+import org.mapstruct.factory.Mappers;
+
+@NullMarked
+@Mapper
+public interface JSpecifyNonNullReturnUpdateIterableMapper {
+
+ JSpecifyNonNullReturnUpdateIterableMapper INSTANCE =
+ Mappers.getMapper( JSpecifyNonNullReturnUpdateIterableMapper.class );
+
+ List mapAll(@Nullable List sources,
+ @MappingTarget List target);
+
+ NullMarkedTargetBean map(NullMarkedSourceBean source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableTest.java
new file mode 100644
index 0000000000..56e71e6995
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.WithJSpecify;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The @NonNull-return forcing is gated on {@code !isUpdateMethod()}. An update (existing-instance) method must not
+ * be forced to RETURN_DEFAULT: a null source returns the supplied target instance, not a fresh empty collection.
+ */
+@IssueKey("1243")
+@WithJSpecify
+class JSpecifyNonNullReturnUpdateIterableTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource();
+
+ @ProcessorTest
+ @WithClasses({
+ NullMarkedSourceBean.class,
+ NullMarkedTargetBean.class,
+ JSpecifyNonNullReturnUpdateIterableMapper.class
+ })
+ void updateMethodWithNonNullReturnIsNotForced() {
+ generatedSource.addComparisonToFixtureFor( JSpecifyNonNullReturnUpdateIterableMapper.class );
+
+ List target = new ArrayList<>();
+
+ List result = JSpecifyNonNullReturnUpdateIterableMapper.INSTANCE.mapAll( null, target );
+
+ assertThat( result ).isSameAs( target );
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapper.java
new file mode 100644
index 0000000000..026890dee8
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapper.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.List;
+
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@NullMarked
+@Mapper
+public interface JSpecifyNullableSourceIterableMapper {
+
+ JSpecifyNullableSourceIterableMapper INSTANCE = Mappers.getMapper( JSpecifyNullableSourceIterableMapper.class );
+
+ @Nullable
+ List mapAll(@Nullable List sources);
+
+ NullMarkedTargetBean map(NullMarkedSourceBean source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableTest.java
new file mode 100644
index 0000000000..da77fe71e1
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.WithJSpecify;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Negative counterpart to {@link JSpecifyIterableMethodTest}: a {@code @Nullable} source parameter (and a
+ * {@code @Nullable} return, so the @NonNull-return forcing does not kick in) must keep the method-level
+ * {@code if ( sources == null ) return null;} guard.
+ */
+@IssueKey("1243")
+@WithJSpecify
+class JSpecifyNullableSourceIterableTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource();
+
+ @ProcessorTest
+ @WithClasses({
+ NullMarkedSourceBean.class,
+ NullMarkedTargetBean.class,
+ JSpecifyNullableSourceIterableMapper.class
+ })
+ void nullableSourceKeepsMethodGuard() {
+ generatedSource.addComparisonToFixtureFor( JSpecifyNullableSourceIterableMapper.class );
+
+ assertThat( JSpecifyNullableSourceIterableMapper.INSTANCE.mapAll( null ) ).isNull();
+
+ NullMarkedSourceBean source = new NullMarkedSourceBean();
+ source.setNonNullByDefault( "value" );
+
+ List targets =
+ JSpecifyNullableSourceIterableMapper.INSTANCE.mapAll( Arrays.asList( source ) );
+
+ assertThat( targets ).hasSize( 1 );
+ assertThat( targets.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" );
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapper.java
new file mode 100644
index 0000000000..387103bae5
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapper.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.List;
+
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+import org.mapstruct.IterableMapping;
+import org.mapstruct.Mapper;
+import org.mapstruct.NullValueMappingStrategy;
+import org.mapstruct.factory.Mappers;
+
+@NullMarked
+@Mapper
+public interface JSpecifyReturnNullOverrideIterableMapper {
+
+ JSpecifyReturnNullOverrideIterableMapper INSTANCE =
+ Mappers.getMapper( JSpecifyReturnNullOverrideIterableMapper.class );
+
+ // Explicit RETURN_NULL, but the @NonNull return type (NullMarked scope) wins and forces RETURN_DEFAULT.
+ @IterableMapping(nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL)
+ List mapAll(@Nullable List sources);
+
+ NullMarkedTargetBean map(NullMarkedSourceBean source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableTest.java
new file mode 100644
index 0000000000..eaea578903
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.WithJSpecify;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verifies that a JSpecify {@code @NonNull} return type wins over an explicitly configured
+ * {@code NullValueMappingStrategy.RETURN_NULL}: the generated method returns an empty collection rather
+ * than {@code null}.
+ */
+@IssueKey("1243")
+@WithJSpecify
+class JSpecifyReturnNullOverrideIterableTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource();
+
+ @ProcessorTest
+ @WithClasses({
+ NullMarkedSourceBean.class,
+ NullMarkedTargetBean.class,
+ JSpecifyReturnNullOverrideIterableMapper.class
+ })
+ void nonNullReturnOverridesExplicitReturnNull() {
+ generatedSource.addComparisonToFixtureFor( JSpecifyReturnNullOverrideIterableMapper.class );
+
+ assertThat( JSpecifyReturnNullOverrideIterableMapper.INSTANCE.mapAll( null ) ).isEmpty();
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapper.java
new file mode 100644
index 0000000000..1b069af5b8
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapper.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.stream.Stream;
+
+import org.jspecify.annotations.NullMarked;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@NullMarked
+@Mapper
+public interface JSpecifyStreamMethodMapper {
+
+ JSpecifyStreamMethodMapper INSTANCE = Mappers.getMapper( JSpecifyStreamMethodMapper.class );
+
+ Stream mapAll(Stream sources);
+
+ NullMarkedTargetBean map(NullMarkedSourceBean source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodTest.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodTest.java
new file mode 100644
index 0000000000..76c7af8792
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.WithJSpecify;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@IssueKey("1243")
+@WithJSpecify
+class JSpecifyStreamMethodTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource();
+
+ @ProcessorTest
+ @WithClasses({
+ NullMarkedSourceBean.class,
+ NullMarkedTargetBean.class,
+ JSpecifyStreamMethodMapper.class
+ })
+ void streamMethodWithNonNullSourceSkipsMethodGuard() {
+ generatedSource.addComparisonToFixtureFor( JSpecifyStreamMethodMapper.class );
+
+ NullMarkedSourceBean source = new NullMarkedSourceBean();
+ source.setNonNullByDefault( "value" );
+
+ List targets = JSpecifyStreamMethodMapper.INSTANCE
+ .mapAll( Stream.of( source ) )
+ .collect( Collectors.toList() );
+
+ assertThat( targets ).hasSize( 1 );
+ assertThat( targets.get( 0 ).getNonNullByDefault() ).isEqualTo( "value" );
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionSourceBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionSourceBean.java
new file mode 100644
index 0000000000..3c67457637
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionSourceBean.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.List;
+
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * Source bean with a {@code @NonNull} collection getter (via @NullMarked scope).
+ */
+@NullMarked
+public class NullMarkedCollectionSourceBean {
+
+ private List values;
+
+ public List getValues() {
+ return values;
+ }
+
+ public void setValues(List values) {
+ this.values = values;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionTargetBean.java b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionTargetBean.java
new file mode 100644
index 0000000000..eae3adc1a9
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/nullcheck/jspecify/NullMarkedCollectionTargetBean.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.List;
+
+import org.jspecify.annotations.NullMarked;
+
+@NullMarked
+public class NullMarkedCollectionTargetBean {
+
+ private List values;
+
+ public List getValues() {
+ return values;
+ }
+
+ public void setValues(List values) {
+ this.values = values;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderMapper.java
new file mode 100644
index 0000000000..bdff608ddc
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderMapper.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.optional.builder.crosspackage;
+
+import java.util.Optional;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.ap.test.optional.builder.crosspackage.dto.PersonTarget;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface CrossPackageOptionalBuilderMapper {
+
+ CrossPackageOptionalBuilderMapper INSTANCE = Mappers.getMapper( CrossPackageOptionalBuilderMapper.class );
+
+ Optional toTarget(PersonSource source);
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderTest.java b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderTest.java
new file mode 100644
index 0000000000..8919787dfb
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/CrossPackageOptionalBuilderTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.optional.builder.crosspackage;
+
+import java.util.Optional;
+
+import org.mapstruct.ap.test.optional.builder.crosspackage.dto.PersonTarget;
+import org.mapstruct.ap.testutil.IssueKey;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@IssueKey( "4046" )
+@WithClasses({ CrossPackageOptionalBuilderMapper.class, PersonSource.class, PersonTarget.class })
+class CrossPackageOptionalBuilderTest {
+
+ @ProcessorTest
+ void shouldUseStaticBuilderMethodForOptionalTargetAcrossPackages() {
+ PersonSource source = new PersonSource( "John" );
+ Optional result = CrossPackageOptionalBuilderMapper.INSTANCE.toTarget( source );
+
+ assertThat( result ).isPresent();
+ assertThat( result.get().getName() ).isEqualTo( "John" );
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/PersonSource.java b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/PersonSource.java
new file mode 100644
index 0000000000..dea0c2f2b8
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/PersonSource.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.optional.builder.crosspackage;
+
+public class PersonSource {
+ private final String name;
+
+ public PersonSource(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/dto/PersonTarget.java b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/dto/PersonTarget.java
new file mode 100644
index 0000000000..6fb5ab770b
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optional/builder/crosspackage/dto/PersonTarget.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.optional.builder.crosspackage.dto;
+
+public class PersonTarget {
+
+ private final String name;
+
+ private PersonTarget(PersonTargetBuilder builder) {
+ this.name = builder.name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public static PersonTargetBuilder builder() {
+ return new PersonTargetBuilder();
+ }
+
+ public static class PersonTargetBuilder {
+
+ private String name;
+
+ PersonTargetBuilder() { }
+
+ public PersonTargetBuilder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public PersonTarget build() {
+ return new PersonTarget( this );
+ }
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/source/nullvaluecheckstrategy/Stage.java b/processor/src/test/java/org/mapstruct/ap/test/source/nullvaluecheckstrategy/Stage.java
index 3a13f0aa1c..be70aa9754 100644
--- a/processor/src/test/java/org/mapstruct/ap/test/source/nullvaluecheckstrategy/Stage.java
+++ b/processor/src/test/java/org/mapstruct/ap/test/source/nullvaluecheckstrategy/Stage.java
@@ -14,9 +14,9 @@
*/
public enum Stage {
- MAIN("Paul McCartney", "Ellie Goulding", "Disclosure", "Kaiser Chiefs", "Rammstein"),
- KLUB_C("James Blake", "Lost Frequencies"),
- THE_BARN("New Order", "Year and Years");
+ MAIN( "Paul McCartney", "Ellie Goulding", "Disclosure", "Kaiser Chiefs", "Rammstein" ),
+ KLUB_C( "James Blake", "Lost Frequencies" ),
+ THE_BARN( "New Order", "Year and Years" );
private final List artists;
diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/assertions/JavaFileAssert.java b/processor/src/test/java/org/mapstruct/ap/testutil/assertions/JavaFileAssert.java
index 2f8b13ba25..277b552f72 100644
--- a/processor/src/test/java/org/mapstruct/ap/testutil/assertions/JavaFileAssert.java
+++ b/processor/src/test/java/org/mapstruct/ap/testutil/assertions/JavaFileAssert.java
@@ -10,6 +10,7 @@
import java.io.UncheckedIOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
@@ -18,6 +19,7 @@
import org.assertj.core.internal.Diff;
import org.assertj.core.internal.Failures;
import org.assertj.core.util.diff.Delta;
+import org.opentest4j.FileInfo;
import static java.lang.String.format;
@@ -81,8 +83,22 @@ public void hasSameMapperContent(File expected) {
) );
diffs.removeIf( this::ignoreDelta );
if ( !diffs.isEmpty() ) {
+ FileInfo actualInfo;
+ FileInfo expectedInfo;
+ try {
+ actualInfo = new FileInfo( actual.getAbsolutePath(), Files.readAllBytes( actual.toPath() ) );
+ expectedInfo = new FileInfo( expected.getAbsolutePath(), Files.readAllBytes( expected.toPath() ) );
+ }
+ catch ( IOException e ) {
+ throw new RuntimeException( e );
+ }
throw Failures.instance()
- .failure( info, ShouldHaveSameContent.shouldHaveSameContent( actual, expected, diffs ) );
+ .failure(
+ info,
+ ShouldHaveSameContent.shouldHaveSameContent( actual, expected, diffs ),
+ actualInfo,
+ expectedInfo
+ );
}
}
catch ( IOException e ) {
diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingExtension.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingExtension.java
index 468e2908a1..eb18615b8b 100644
--- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingExtension.java
+++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingExtension.java
@@ -674,7 +674,7 @@ boolean needsRecompilation(CompilationRequest compilationRequest, CompilationCac
return !compilationRequest.equals( compilationCache.getLastRequest() );
}
- private static String getBasePath() {
+ static String getBasePath() {
try {
return new File( "." ).getCanonicalPath();
}
diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/GeneratedSource.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/GeneratedSource.java
index 85b6ec80fd..7c6c447d77 100644
--- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/GeneratedSource.java
+++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/GeneratedSource.java
@@ -6,9 +6,6 @@
package org.mapstruct.ap.testutil.runner;
import java.io.File;
-import java.net.URL;
-import java.net.URLDecoder;
-import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@@ -19,6 +16,7 @@
import static org.assertj.core.api.Assertions.fail;
import static org.mapstruct.ap.testutil.runner.CompilingExtension.NAMESPACE;
+import static org.mapstruct.ap.testutil.runner.CompilingExtension.getBasePath;
/**
* A {@link org.junit.jupiter.api.extension.RegisterExtension RegisterExtension} to perform assertions on generated
@@ -36,6 +34,7 @@
public class GeneratedSource implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private static final String FIXTURES_ROOT = "fixtures/";
+ private static final String FIXTURES_DIR = getBasePath() + "/src/test/resources/fixtures/";
/**
* ThreadLocal, as the source dir must be injected for this extension to gain access
@@ -139,8 +138,8 @@ public JavaFileAssert forJavaFile(String path) {
private void handleFixtureComparison() {
for ( FixtureComparison fixture : fixturesFor ) {
String fixtureName = getFixtureName( fixture.mapperClass, fixture.variant );
- URL expectedFile = getExpectedResource( fixtureName );
- if ( expectedFile == null ) {
+ File expectedFile = getExpectedResource( fixtureName );
+ if ( !expectedFile.exists() ) {
fail( String.format(
"No reference file could be found for Mapper %s. You should create a file %s",
fixture.mapperClass.getName(),
@@ -148,8 +147,7 @@ private void handleFixtureComparison() {
) );
}
else {
- File expectedResource = new File( URLDecoder.decode( expectedFile.getFile(), StandardCharsets.UTF_8 ) );
- forMapper( fixture.mapperClass ).hasSameMapperContent( expectedResource );
+ forMapper( fixture.mapperClass ).hasSameMapperContent( expectedFile );
}
}
}
@@ -159,16 +157,15 @@ private String getFixtureName(Class> mapperClass, String variant) {
return mapperClass.getName().replace( '.', '/' ).concat( suffix );
}
- private URL getExpectedResource(String fixtureName) {
- ClassLoader classLoader = getClass().getClassLoader();
+ private File getExpectedResource(String fixtureName) {
for ( int version = Runtime.version().feature(); version >= 11 && compiler != Compiler.ECLIPSE; version-- ) {
- URL resource = classLoader.getResource( FIXTURES_ROOT + "/" + version + "/" + fixtureName );
- if ( resource != null ) {
- return resource;
+ File fixtureFile = new File( FIXTURES_DIR + version + File.separator + fixtureName );
+ if ( fixtureFile.exists() ) {
+ return fixtureFile;
}
}
- return classLoader.getResource( FIXTURES_ROOT + fixtureName );
+ return new File( FIXTURES_DIR + fixtureName );
}
private static final class FixtureComparison {
diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoMapperClassicImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoMapperClassicImpl.java
new file mode 100644
index 0000000000..408a83c145
--- /dev/null
+++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoMapperClassicImpl.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nestedbeans;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-12T22:15:18+0200",
+ comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)"
+)
+public class UserDtoMapperClassicImpl implements UserDtoMapperClassic {
+
+ @Override
+ public UserDto userToUserDto(User user) {
+ if ( user == null ) {
+ return null;
+ }
+
+ UserDto userDto = new UserDto();
+
+ userDto.setName( user.getName() );
+ userDto.setCar( carToCarDto( user.getCar() ) );
+ userDto.setSecondCar( carToCarDto( user.getSecondCar() ) );
+ userDto.setHouse( houseToHouseDto( user.getHouse() ) );
+
+ return userDto;
+ }
+
+ @Override
+ public CarDto carToCarDto(Car car) {
+ if ( car == null ) {
+ return null;
+ }
+
+ CarDto carDto = new CarDto();
+
+ carDto.setName( car.getName() );
+ carDto.setYear( car.getYear() );
+ carDto.setWheels( mapWheels( car.getWheels() ) );
+
+ return carDto;
+ }
+
+ @Override
+ public HouseDto houseToHouseDto(House house) {
+ if ( house == null ) {
+ return null;
+ }
+
+ HouseDto houseDto = new HouseDto();
+
+ houseDto.setName( house.getName() );
+ houseDto.setYear( house.getYear() );
+ houseDto.setRoof( roofToRoofDto( house.getRoof() ) );
+
+ return houseDto;
+ }
+
+ @Override
+ public RoofDto roofToRoofDto(Roof roof) {
+ if ( roof == null ) {
+ return null;
+ }
+
+ RoofDto roofDto = new RoofDto();
+
+ roofDto.setColor( String.valueOf( roof.getColor() ) );
+ roofDto.setType( mapRoofType( roof.getType() ) );
+
+ return roofDto;
+ }
+
+ @Override
+ public List mapWheels(List wheels) {
+ if ( wheels == null ) {
+ return null;
+ }
+
+ List list = new ArrayList<>( wheels.size() );
+ for ( Wheel wheel : wheels ) {
+ list.add( mapWheel( wheel ) );
+ }
+
+ return list;
+ }
+
+ @Override
+ public WheelDto mapWheel(Wheel wheels) {
+ if ( wheels == null ) {
+ return null;
+ }
+
+ WheelDto wheelDto = new WheelDto();
+
+ wheelDto.setFront( wheels.isFront() );
+ wheelDto.setRight( wheels.isRight() );
+
+ return wheelDto;
+ }
+
+ @Override
+ public ExternalRoofType mapRoofType(RoofType roofType) {
+ if ( roofType == null ) {
+ return null;
+ }
+
+ ExternalRoofType externalRoofType = switch ( roofType ) {
+ case OPEN -> ExternalRoofType.OPEN;
+ case BOX -> ExternalRoofType.BOX;
+ case GAMBREL -> ExternalRoofType.GAMBREL;
+ };
+
+ return externalRoofType;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoMapperSmartImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoMapperSmartImpl.java
new file mode 100644
index 0000000000..f20d3be4ef
--- /dev/null
+++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoMapperSmartImpl.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nestedbeans;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-12T22:22:12+0200",
+ comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)"
+)
+public class UserDtoMapperSmartImpl implements UserDtoMapperSmart {
+
+ @Override
+ public UserDto userToUserDto(User user) {
+ if ( user == null ) {
+ return null;
+ }
+
+ UserDto userDto = new UserDto();
+
+ userDto.setName( user.getName() );
+ userDto.setCar( carToCarDto( user.getCar() ) );
+ userDto.setSecondCar( carToCarDto( user.getSecondCar() ) );
+ userDto.setHouse( houseToHouseDto( user.getHouse() ) );
+
+ return userDto;
+ }
+
+ @Override
+ public org.mapstruct.ap.test.nestedbeans.other.UserDto userToUserDto2(User user) {
+ if ( user == null ) {
+ return null;
+ }
+
+ org.mapstruct.ap.test.nestedbeans.other.UserDto userDto = new org.mapstruct.ap.test.nestedbeans.other.UserDto();
+
+ userDto.setName( user.getName() );
+ userDto.setCar( carToCarDto1( user.getCar() ) );
+ userDto.setHouse( houseToHouseDto1( user.getHouse() ) );
+
+ return userDto;
+ }
+
+ protected WheelDto wheelToWheelDto(Wheel wheel) {
+ if ( wheel == null ) {
+ return null;
+ }
+
+ WheelDto wheelDto = new WheelDto();
+
+ wheelDto.setFront( wheel.isFront() );
+ wheelDto.setRight( wheel.isRight() );
+
+ return wheelDto;
+ }
+
+ protected List wheelListToWheelDtoList(List list) {
+ if ( list == null ) {
+ return null;
+ }
+
+ List list1 = new ArrayList<>( list.size() );
+ for ( Wheel wheel : list ) {
+ list1.add( wheelToWheelDto( wheel ) );
+ }
+
+ return list1;
+ }
+
+ protected CarDto carToCarDto(Car car) {
+ if ( car == null ) {
+ return null;
+ }
+
+ CarDto carDto = new CarDto();
+
+ carDto.setName( car.getName() );
+ carDto.setYear( car.getYear() );
+ carDto.setWheels( wheelListToWheelDtoList( car.getWheels() ) );
+
+ return carDto;
+ }
+
+ protected ExternalRoofType roofTypeToExternalRoofType(RoofType roofType) {
+ if ( roofType == null ) {
+ return null;
+ }
+
+ ExternalRoofType externalRoofType = switch ( roofType ) {
+ case OPEN -> ExternalRoofType.OPEN;
+ case BOX -> ExternalRoofType.BOX;
+ case GAMBREL -> ExternalRoofType.GAMBREL;
+ };
+
+ return externalRoofType;
+ }
+
+ protected RoofDto roofToRoofDto(Roof roof) {
+ if ( roof == null ) {
+ return null;
+ }
+
+ RoofDto roofDto = new RoofDto();
+
+ roofDto.setColor( String.valueOf( roof.getColor() ) );
+ roofDto.setType( roofTypeToExternalRoofType( roof.getType() ) );
+
+ return roofDto;
+ }
+
+ protected HouseDto houseToHouseDto(House house) {
+ if ( house == null ) {
+ return null;
+ }
+
+ HouseDto houseDto = new HouseDto();
+
+ houseDto.setName( house.getName() );
+ houseDto.setYear( house.getYear() );
+ houseDto.setRoof( roofToRoofDto( house.getRoof() ) );
+
+ return houseDto;
+ }
+
+ protected org.mapstruct.ap.test.nestedbeans.other.WheelDto wheelToWheelDto1(Wheel wheel) {
+ if ( wheel == null ) {
+ return null;
+ }
+
+ org.mapstruct.ap.test.nestedbeans.other.WheelDto wheelDto = new org.mapstruct.ap.test.nestedbeans.other.WheelDto();
+
+ wheelDto.setFront( wheel.isFront() );
+ wheelDto.setRight( wheel.isRight() );
+
+ return wheelDto;
+ }
+
+ protected List wheelListToWheelDtoList1(List list) {
+ if ( list == null ) {
+ return null;
+ }
+
+ List list1 = new ArrayList<>( list.size() );
+ for ( Wheel wheel : list ) {
+ list1.add( wheelToWheelDto1( wheel ) );
+ }
+
+ return list1;
+ }
+
+ protected org.mapstruct.ap.test.nestedbeans.other.CarDto carToCarDto1(Car car) {
+ if ( car == null ) {
+ return null;
+ }
+
+ org.mapstruct.ap.test.nestedbeans.other.CarDto carDto = new org.mapstruct.ap.test.nestedbeans.other.CarDto();
+
+ carDto.setName( car.getName() );
+ carDto.setYear( car.getYear() );
+ carDto.setWheels( wheelListToWheelDtoList1( car.getWheels() ) );
+
+ return carDto;
+ }
+
+ protected org.mapstruct.ap.test.nestedbeans.other.RoofDto roofToRoofDto1(Roof roof) {
+ if ( roof == null ) {
+ return null;
+ }
+
+ org.mapstruct.ap.test.nestedbeans.other.RoofDto roofDto = new org.mapstruct.ap.test.nestedbeans.other.RoofDto();
+
+ roofDto.setColor( String.valueOf( roof.getColor() ) );
+
+ return roofDto;
+ }
+
+ protected org.mapstruct.ap.test.nestedbeans.other.HouseDto houseToHouseDto1(House house) {
+ if ( house == null ) {
+ return null;
+ }
+
+ org.mapstruct.ap.test.nestedbeans.other.HouseDto houseDto = new org.mapstruct.ap.test.nestedbeans.other.HouseDto();
+
+ houseDto.setName( house.getName() );
+ houseDto.setYear( house.getYear() );
+ houseDto.setRoof( roofToRoofDto1( house.getRoof() ) );
+
+ return houseDto;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoUpdateMapperSmartImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoUpdateMapperSmartImpl.java
new file mode 100644
index 0000000000..1b82ddc4ee
--- /dev/null
+++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nestedbeans/UserDtoUpdateMapperSmartImpl.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nestedbeans;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-12T22:22:13+0200",
+ comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)"
+)
+public class UserDtoUpdateMapperSmartImpl implements UserDtoUpdateMapperSmart {
+
+ @Override
+ public void userToUserDto(UserDto userDto, User user) {
+ if ( user == null ) {
+ return;
+ }
+
+ if ( user.getName() != null ) {
+ userDto.setName( user.getName() );
+ }
+ else {
+ userDto.setName( null );
+ }
+ if ( user.getCar() != null ) {
+ if ( userDto.getCar() == null ) {
+ userDto.setCar( new CarDto() );
+ }
+ carToCarDto( user.getCar(), userDto.getCar() );
+ }
+ else {
+ userDto.setCar( null );
+ }
+ if ( user.getSecondCar() != null ) {
+ if ( userDto.getSecondCar() == null ) {
+ userDto.setSecondCar( new CarDto() );
+ }
+ carToCarDto( user.getSecondCar(), userDto.getSecondCar() );
+ }
+ else {
+ userDto.setSecondCar( null );
+ }
+ if ( user.getHouse() != null ) {
+ if ( userDto.getHouse() == null ) {
+ userDto.setHouse( new HouseDto() );
+ }
+ houseToHouseDto( user.getHouse(), userDto.getHouse() );
+ }
+ else {
+ userDto.setHouse( null );
+ }
+ }
+
+ protected WheelDto wheelToWheelDto(Wheel wheel) {
+ if ( wheel == null ) {
+ return null;
+ }
+
+ WheelDto wheelDto = new WheelDto();
+
+ wheelDto.setFront( wheel.isFront() );
+ wheelDto.setRight( wheel.isRight() );
+
+ return wheelDto;
+ }
+
+ protected List wheelListToWheelDtoList(List list) {
+ if ( list == null ) {
+ return null;
+ }
+
+ List list1 = new ArrayList<>( list.size() );
+ for ( Wheel wheel : list ) {
+ list1.add( wheelToWheelDto( wheel ) );
+ }
+
+ return list1;
+ }
+
+ protected void carToCarDto(Car car, CarDto mappingTarget) {
+ if ( car == null ) {
+ return;
+ }
+
+ if ( car.getName() != null ) {
+ mappingTarget.setName( car.getName() );
+ }
+ else {
+ mappingTarget.setName( null );
+ }
+ mappingTarget.setYear( car.getYear() );
+ if ( mappingTarget.getWheels() != null ) {
+ List list = wheelListToWheelDtoList( car.getWheels() );
+ if ( list != null ) {
+ mappingTarget.getWheels().clear();
+ mappingTarget.getWheels().addAll( list );
+ }
+ }
+ else {
+ List list = wheelListToWheelDtoList( car.getWheels() );
+ if ( list != null ) {
+ mappingTarget.setWheels( list );
+ }
+ }
+ }
+
+ protected ExternalRoofType roofTypeToExternalRoofType(RoofType roofType) {
+ if ( roofType == null ) {
+ return null;
+ }
+
+ ExternalRoofType externalRoofType = switch ( roofType ) {
+ case OPEN -> ExternalRoofType.OPEN;
+ case BOX -> ExternalRoofType.BOX;
+ case GAMBREL -> ExternalRoofType.GAMBREL;
+ };
+
+ return externalRoofType;
+ }
+
+ protected void roofToRoofDto(Roof roof, RoofDto mappingTarget) {
+ if ( roof == null ) {
+ return;
+ }
+
+ mappingTarget.setColor( String.valueOf( roof.getColor() ) );
+ if ( roof.getType() != null ) {
+ mappingTarget.setType( roofTypeToExternalRoofType( roof.getType() ) );
+ }
+ else {
+ mappingTarget.setType( null );
+ }
+ }
+
+ protected void houseToHouseDto(House house, HouseDto mappingTarget) {
+ if ( house == null ) {
+ return;
+ }
+
+ if ( house.getName() != null ) {
+ mappingTarget.setName( house.getName() );
+ }
+ else {
+ mappingTarget.setName( null );
+ }
+ mappingTarget.setYear( house.getYear() );
+ if ( house.getRoof() != null ) {
+ if ( mappingTarget.getRoof() == null ) {
+ mappingTarget.setRoof( new RoofDto() );
+ }
+ roofToRoofDto( house.getRoof(), mappingTarget.getRoof() );
+ }
+ else {
+ mappingTarget.setRoof( null );
+ }
+ }
+}
diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapperImpl.java
new file mode 100644
index 0000000000..a655a662dc
--- /dev/null
+++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapperImpl.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-05-14T23:31:18+0200",
+ comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)"
+)
+public class JSpecifyMapMethodMapperImpl implements JSpecifyMapMethodMapper {
+
+ @Override
+ public Map mapAll(Map sources) {
+
+ Map map = LinkedHashMap.newLinkedHashMap( sources.size() );
+
+ for ( java.util.Map.Entry entry : sources.entrySet() ) {
+ String key = entry.getKey();
+ NullMarkedTargetBean value = map( entry.getValue() );
+ map.put( key, value );
+ }
+
+ return map;
+ }
+
+ @Override
+ public NullMarkedTargetBean map(NullMarkedSourceBean source) {
+
+ NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean();
+
+ nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() );
+ nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() );
+
+ return nullMarkedTargetBean;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapperImpl.java
new file mode 100644
index 0000000000..eefeafd349
--- /dev/null
+++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapperImpl.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-05-15T00:03:32+0200",
+ comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)"
+)
+public class JSpecifyNonNullReturnMapMapperImpl implements JSpecifyNonNullReturnMapMapper {
+
+ @Override
+ public Map mapAll(Map sources) {
+ if ( sources == null ) {
+ return new LinkedHashMap<>();
+ }
+
+ Map map = LinkedHashMap.newLinkedHashMap( sources.size() );
+
+ for ( java.util.Map.Entry entry : sources.entrySet() ) {
+ String key = entry.getKey();
+ NullMarkedTargetBean value = map( entry.getValue() );
+ map.put( key, value );
+ }
+
+ return map;
+ }
+
+ @Override
+ public NullMarkedTargetBean map(NullMarkedSourceBean source) {
+
+ NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean();
+
+ nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() );
+ nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() );
+
+ return nullMarkedTargetBean;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/DefaultOrderMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/DefaultOrderMapperImpl.java
new file mode 100644
index 0000000000..864accaa9e
--- /dev/null
+++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/DefaultOrderMapperImpl.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.value.enum2enum;
+
+import javax.annotation.processing.Generated;
+import org.mapstruct.ap.test.value.ExternalOrderType;
+import org.mapstruct.ap.test.value.OrderType;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-05T22:27:58+0200",
+ comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)"
+)
+public class DefaultOrderMapperImpl implements DefaultOrderMapper {
+
+ @Override
+ public OrderDto orderEntityToDto(OrderEntity order) {
+ if ( order == null ) {
+ return null;
+ }
+
+ OrderDto orderDto = new OrderDto();
+
+ orderDto.setOrderType( orderTypeToExternalOrderType( order.getOrderType() ) );
+
+ return orderDto;
+ }
+
+ @Override
+ public ExternalOrderType orderTypeToExternalOrderType(OrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ ExternalOrderType externalOrderType = ExternalOrderType.DEFAULT;
+
+ return externalOrderType;
+ }
+}
\ No newline at end of file
diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/OrderMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/OrderMapperImpl.java
new file mode 100644
index 0000000000..61e6bb3f03
--- /dev/null
+++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/OrderMapperImpl.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.value.enum2enum;
+
+import javax.annotation.processing.Generated;
+import org.mapstruct.ap.test.value.ExternalOrderType;
+import org.mapstruct.ap.test.value.OrderType;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-12T14:19:13+0200",
+ comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)"
+)
+public class OrderMapperImpl implements OrderMapper {
+
+ @Override
+ public OrderDto orderEntityToDto(OrderEntity order) {
+ if ( order == null ) {
+ return null;
+ }
+
+ OrderDto orderDto = new OrderDto();
+
+ orderDto.setOrderType( orderTypeToExternalOrderType( order.getOrderType() ) );
+
+ return orderDto;
+ }
+
+ @Override
+ public ExternalOrderType orderTypeToExternalOrderType(OrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ ExternalOrderType externalOrderType = switch ( orderType ) {
+ case EXTRA -> ExternalOrderType.SPECIAL;
+ case STANDARD -> ExternalOrderType.DEFAULT;
+ case NORMAL -> ExternalOrderType.DEFAULT;
+ case RETAIL -> ExternalOrderType.RETAIL;
+ case B2B -> ExternalOrderType.B2B;
+ };
+
+ return externalOrderType;
+ }
+
+ @Override
+ public OrderType externalOrderTypeToOrderType(ExternalOrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ OrderType orderType1 = switch ( orderType ) {
+ case SPECIAL -> OrderType.EXTRA;
+ case DEFAULT -> OrderType.STANDARD;
+ case RETAIL -> OrderType.RETAIL;
+ case B2B -> OrderType.B2B;
+ };
+
+ return orderType1;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/SpecialOrderMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/SpecialOrderMapperImpl.java
new file mode 100644
index 0000000000..223485c327
--- /dev/null
+++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/enum2enum/SpecialOrderMapperImpl.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.value.enum2enum;
+
+import javax.annotation.processing.Generated;
+import org.mapstruct.ap.test.value.ExternalOrderType;
+import org.mapstruct.ap.test.value.OrderType;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-12T16:23:42+0200",
+ comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)"
+)
+public class SpecialOrderMapperImpl implements SpecialOrderMapper {
+
+ @Override
+ public OrderDto orderEntityToDto(OrderEntity order) {
+ if ( order == null ) {
+ return null;
+ }
+
+ OrderDto orderDto = new OrderDto();
+
+ orderDto.setOrderType( orderTypeToExternalOrderType( order.getOrderType() ) );
+
+ return orderDto;
+ }
+
+ @Override
+ public ExternalOrderType orderTypeToExternalOrderType(OrderType orderType) {
+ if ( orderType == null ) {
+ return ExternalOrderType.DEFAULT;
+ }
+
+ ExternalOrderType externalOrderType = switch ( orderType ) {
+ case STANDARD -> null;
+ case RETAIL -> ExternalOrderType.RETAIL;
+ case B2B -> ExternalOrderType.B2B;
+ default -> ExternalOrderType.SPECIAL;
+ };
+
+ return externalOrderType;
+ }
+
+ @Override
+ public OrderType externalOrderTypeToOrderType(ExternalOrderType orderType) {
+ if ( orderType == null ) {
+ return OrderType.STANDARD;
+ }
+
+ OrderType orderType1 = switch ( orderType ) {
+ case SPECIAL -> OrderType.EXTRA;
+ case DEFAULT -> null;
+ case RETAIL -> OrderType.RETAIL;
+ case B2B -> OrderType.B2B;
+ };
+
+ return orderType1;
+ }
+
+ @Override
+ public ExternalOrderType anyRemainingToNull(OrderType orderType) {
+ if ( orderType == null ) {
+ return ExternalOrderType.DEFAULT;
+ }
+
+ ExternalOrderType externalOrderType = switch ( orderType ) {
+ case STANDARD -> null;
+ case RETAIL -> ExternalOrderType.RETAIL;
+ case B2B -> ExternalOrderType.B2B;
+ default -> null;
+ };
+
+ return externalOrderType;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperAndEnumMappingImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperAndEnumMappingImpl.java
new file mode 100644
index 0000000000..bfa7fc1fdc
--- /dev/null
+++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperAndEnumMappingImpl.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.value.exception;
+
+import javax.annotation.processing.Generated;
+import org.mapstruct.ap.test.value.ExternalOrderType;
+import org.mapstruct.ap.test.value.OrderType;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-12T16:23:39+0200",
+ comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)"
+)
+public class CustomUnexpectedValueMappingExceptionDefinedInMapperAndEnumMappingImpl implements CustomUnexpectedValueMappingExceptionDefinedInMapperAndEnumMapping {
+
+ @Override
+ public ExternalOrderType withAnyUnmapped(OrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ ExternalOrderType externalOrderType = ExternalOrderType.DEFAULT;
+
+ return externalOrderType;
+ }
+
+ @Override
+ public ExternalOrderType withAnyRemaining(OrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ ExternalOrderType externalOrderType = switch ( orderType ) {
+ case RETAIL -> ExternalOrderType.RETAIL;
+ case B2B -> ExternalOrderType.B2B;
+ default -> ExternalOrderType.DEFAULT;
+ };
+
+ return externalOrderType;
+ }
+
+ @Override
+ public ExternalOrderType onlyWithMappings(OrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ ExternalOrderType externalOrderType = switch ( orderType ) {
+ case EXTRA -> ExternalOrderType.SPECIAL;
+ case STANDARD -> ExternalOrderType.DEFAULT;
+ case NORMAL -> ExternalOrderType.DEFAULT;
+ case RETAIL -> ExternalOrderType.RETAIL;
+ case B2B -> ExternalOrderType.B2B;
+ };
+
+ return externalOrderType;
+ }
+
+ @Override
+ public OrderType inverseOnlyWithMappings(ExternalOrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ OrderType orderType1 = switch ( orderType ) {
+ case SPECIAL -> OrderType.EXTRA;
+ case DEFAULT -> OrderType.STANDARD;
+ case RETAIL -> OrderType.RETAIL;
+ case B2B -> OrderType.B2B;
+ };
+
+ return orderType1;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperConfigImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperConfigImpl.java
new file mode 100644
index 0000000000..641ff95b17
--- /dev/null
+++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperConfigImpl.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.value.exception;
+
+import javax.annotation.processing.Generated;
+import org.mapstruct.ap.test.value.ExternalOrderType;
+import org.mapstruct.ap.test.value.OrderType;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-12T22:01:43+0200",
+ comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)"
+)
+public class CustomUnexpectedValueMappingExceptionDefinedInMapperConfigImpl implements CustomUnexpectedValueMappingExceptionDefinedInMapperConfig {
+
+ @Override
+ public ExternalOrderType withAnyUnmapped(OrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ ExternalOrderType externalOrderType = ExternalOrderType.DEFAULT;
+
+ return externalOrderType;
+ }
+
+ @Override
+ public ExternalOrderType withAnyRemaining(OrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ ExternalOrderType externalOrderType = switch ( orderType ) {
+ case RETAIL -> ExternalOrderType.RETAIL;
+ case B2B -> ExternalOrderType.B2B;
+ default -> ExternalOrderType.DEFAULT;
+ };
+
+ return externalOrderType;
+ }
+
+ @Override
+ public ExternalOrderType onlyWithMappings(OrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ ExternalOrderType externalOrderType = switch ( orderType ) {
+ case EXTRA -> ExternalOrderType.SPECIAL;
+ case STANDARD -> ExternalOrderType.DEFAULT;
+ case NORMAL -> ExternalOrderType.DEFAULT;
+ case RETAIL -> ExternalOrderType.RETAIL;
+ case B2B -> ExternalOrderType.B2B;
+ };
+
+ return externalOrderType;
+ }
+
+ @Override
+ public OrderType inverseOnlyWithMappings(ExternalOrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ OrderType orderType1 = switch ( orderType ) {
+ case SPECIAL -> OrderType.EXTRA;
+ case DEFAULT -> OrderType.STANDARD;
+ case RETAIL -> OrderType.RETAIL;
+ case B2B -> OrderType.B2B;
+ };
+
+ return orderType1;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperImpl.java
new file mode 100644
index 0000000000..2f47b74ae4
--- /dev/null
+++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionDefinedInMapperImpl.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.value.exception;
+
+import javax.annotation.processing.Generated;
+import org.mapstruct.ap.test.value.ExternalOrderType;
+import org.mapstruct.ap.test.value.OrderType;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-12T14:19:13+0200",
+ comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)"
+)
+public class CustomUnexpectedValueMappingExceptionDefinedInMapperImpl implements CustomUnexpectedValueMappingExceptionDefinedInMapper {
+
+ @Override
+ public ExternalOrderType withAnyUnmapped(OrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ ExternalOrderType externalOrderType = ExternalOrderType.DEFAULT;
+
+ return externalOrderType;
+ }
+
+ @Override
+ public ExternalOrderType withAnyRemaining(OrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ ExternalOrderType externalOrderType = switch ( orderType ) {
+ case RETAIL -> ExternalOrderType.RETAIL;
+ case B2B -> ExternalOrderType.B2B;
+ default -> ExternalOrderType.DEFAULT;
+ };
+
+ return externalOrderType;
+ }
+
+ @Override
+ public ExternalOrderType onlyWithMappings(OrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ ExternalOrderType externalOrderType = switch ( orderType ) {
+ case EXTRA -> ExternalOrderType.SPECIAL;
+ case STANDARD -> ExternalOrderType.DEFAULT;
+ case NORMAL -> ExternalOrderType.DEFAULT;
+ case RETAIL -> ExternalOrderType.RETAIL;
+ case B2B -> ExternalOrderType.B2B;
+ };
+
+ return externalOrderType;
+ }
+
+ @Override
+ public OrderType inverseOnlyWithMappings(ExternalOrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ OrderType orderType1 = switch ( orderType ) {
+ case SPECIAL -> OrderType.EXTRA;
+ case DEFAULT -> OrderType.STANDARD;
+ case RETAIL -> OrderType.RETAIL;
+ case B2B -> OrderType.B2B;
+ };
+
+ return orderType1;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionMapperImpl.java
new file mode 100644
index 0000000000..ef5c3f9bfa
--- /dev/null
+++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/exception/CustomUnexpectedValueMappingExceptionMapperImpl.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.value.exception;
+
+import javax.annotation.processing.Generated;
+import org.mapstruct.ap.test.value.ExternalOrderType;
+import org.mapstruct.ap.test.value.OrderType;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-12T22:04:26+0200",
+ comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)"
+)
+public class CustomUnexpectedValueMappingExceptionMapperImpl implements CustomUnexpectedValueMappingExceptionMapper {
+
+ @Override
+ public ExternalOrderType withAnyUnmapped(OrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ ExternalOrderType externalOrderType = ExternalOrderType.DEFAULT;
+
+ return externalOrderType;
+ }
+
+ @Override
+ public ExternalOrderType withAnyRemaining(OrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ ExternalOrderType externalOrderType = switch ( orderType ) {
+ case RETAIL -> ExternalOrderType.RETAIL;
+ case B2B -> ExternalOrderType.B2B;
+ default -> ExternalOrderType.DEFAULT;
+ };
+
+ return externalOrderType;
+ }
+
+ @Override
+ public ExternalOrderType onlyWithMappings(OrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ ExternalOrderType externalOrderType = switch ( orderType ) {
+ case EXTRA -> ExternalOrderType.SPECIAL;
+ case STANDARD -> ExternalOrderType.DEFAULT;
+ case NORMAL -> ExternalOrderType.DEFAULT;
+ case RETAIL -> ExternalOrderType.RETAIL;
+ case B2B -> ExternalOrderType.B2B;
+ };
+
+ return externalOrderType;
+ }
+
+ @Override
+ public OrderType inverseOnlyWithMappings(ExternalOrderType orderType) {
+ if ( orderType == null ) {
+ return null;
+ }
+
+ OrderType orderType1 = switch ( orderType ) {
+ case SPECIAL -> OrderType.EXTRA;
+ case DEFAULT -> OrderType.STANDARD;
+ case RETAIL -> OrderType.RETAIL;
+ case B2B -> OrderType.B2B;
+ };
+
+ return orderType1;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/spi/CustomCheeseMapperImpl.java b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/spi/CustomCheeseMapperImpl.java
new file mode 100644
index 0000000000..94ac662859
--- /dev/null
+++ b/processor/src/test/resources/fixtures/21/org/mapstruct/ap/test/value/spi/CustomCheeseMapperImpl.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.value.spi;
+
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-12T14:19:17+0200",
+ comments = "version: , compiler: javac, environment: Java 21.0.11 (Eclipse Adoptium)"
+)
+public class CustomCheeseMapperImpl implements CustomCheeseMapper {
+
+ @Override
+ public CheeseType map(CustomCheeseType cheese) {
+ if ( cheese == null ) {
+ return null;
+ }
+
+ CheeseType cheeseType = switch ( cheese ) {
+ case UNSPECIFIED -> null;
+ case CUSTOM_BRIE -> CheeseType.BRIE;
+ case CUSTOM_ROQUEFORT -> CheeseType.ROQUEFORT;
+ case UNRECOGNIZED -> null;
+ };
+
+ return cheeseType;
+ }
+
+ @Override
+ public CustomCheeseType map(CheeseType cheese) {
+ if ( cheese == null ) {
+ return CustomCheeseType.UNSPECIFIED;
+ }
+
+ CustomCheeseType customCheeseType = switch ( cheese ) {
+ case BRIE -> CustomCheeseType.CUSTOM_BRIE;
+ case ROQUEFORT -> CustomCheeseType.CUSTOM_ROQUEFORT;
+ };
+
+ return customCheeseType;
+ }
+
+ @Override
+ public String mapToString(CustomCheeseType cheeseType) {
+ if ( cheeseType == null ) {
+ return null;
+ }
+
+ String string = switch ( cheeseType ) {
+ case UNSPECIFIED -> null;
+ case CUSTOM_BRIE -> "BRIE";
+ case CUSTOM_ROQUEFORT -> "ROQUEFORT";
+ case UNRECOGNIZED -> null;
+ };
+
+ return string;
+ }
+
+ @Override
+ public String mapToString(CheeseType cheeseType) {
+ if ( cheeseType == null ) {
+ return null;
+ }
+
+ String string = switch ( cheeseType ) {
+ case BRIE -> "BRIE";
+ case ROQUEFORT -> "ROQUEFORT";
+ };
+
+ return string;
+ }
+
+ @Override
+ public CustomCheeseType mapStringToCustom(String cheese) {
+ if ( cheese == null ) {
+ return CustomCheeseType.UNSPECIFIED;
+ }
+
+ CustomCheeseType customCheeseType = switch ( cheese ) {
+ case "BRIE" -> CustomCheeseType.CUSTOM_BRIE;
+ case "ROQUEFORT" -> CustomCheeseType.CUSTOM_ROQUEFORT;
+ default -> CustomCheeseType.CUSTOM_BRIE;
+ };
+
+ return customCheeseType;
+ }
+
+ @Override
+ public CheeseType mapStringToCheese(String cheese) {
+ if ( cheese == null ) {
+ return null;
+ }
+
+ CheeseType cheeseType = switch ( cheese ) {
+ case "BRIE" -> CheeseType.BRIE;
+ case "ROQUEFORT" -> CheeseType.ROQUEFORT;
+ default -> CheeseType.BRIE;
+ };
+
+ return cheeseType;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapperImpl.java
new file mode 100644
index 0000000000..71f9998364
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/conversion/java8time/zonedoffsetdatetimetolocaldatetimeconversion/SourceTargetMapperImpl.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.conversion.java8time.zonedoffsetdatetimetolocaldatetimeconversion;
+
+import java.time.ZoneOffset;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-05-23T21:55:42+0900",
+ comments = "version: , compiler: javac, environment: Java 25.0.2 (Homebrew)"
+)
+public class SourceTargetMapperImpl implements SourceTargetMapper {
+
+ @Override
+ public Target toTarget(Source source) {
+ if ( source == null ) {
+ return null;
+ }
+
+ Target target = new Target();
+
+ if ( source.getOffsetDateTime() != null ) {
+ target.setOffsetDateTime( source.getOffsetDateTime().toLocalDateTime() );
+ }
+ if ( source.getZonedDateTime() != null ) {
+ target.setZonedDateTime( source.getZonedDateTime().toLocalDateTime() );
+ }
+ if ( source.getZonedDateTimeAsInstant() != null ) {
+ target.setZonedDateTimeAsInstant( source.getZonedDateTimeAsInstant().toInstant() );
+ }
+ if ( source.getOffsetDateTimeAsInstant() != null ) {
+ target.setOffsetDateTimeAsInstant( source.getOffsetDateTimeAsInstant().toInstant() );
+ }
+
+ return target;
+ }
+
+ @Override
+ public Source toSource(Target target) {
+ if ( target == null ) {
+ return null;
+ }
+
+ Source source = new Source();
+
+ if ( target.getZonedDateTime() != null ) {
+ source.setZonedDateTime( target.getZonedDateTime().atZone( ZoneOffset.UTC ) );
+ }
+ if ( target.getOffsetDateTime() != null ) {
+ source.setOffsetDateTime( target.getOffsetDateTime().atOffset( ZoneOffset.UTC ) );
+ }
+ if ( target.getZonedDateTimeAsInstant() != null ) {
+ source.setZonedDateTimeAsInstant( target.getZonedDateTimeAsInstant().atZone( ZoneOffset.UTC ) );
+ }
+ if ( target.getOffsetDateTimeAsInstant() != null ) {
+ source.setOffsetDateTimeAsInstant( target.getOffsetDateTimeAsInstant().atOffset( ZoneOffset.UTC ) );
+ }
+
+ return source;
+ }
+}
\ No newline at end of file
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapperImpl.java
new file mode 100644
index 0000000000..4004bc60fe
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyCollectionPropertyMapperImpl.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-05-14T23:05:10+0200",
+ comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)"
+)
+public class JSpecifyCollectionPropertyMapperImpl implements JSpecifyCollectionPropertyMapper {
+
+ @Override
+ public NullMarkedCollectionTargetBean map(NullMarkedCollectionSourceBean source) {
+
+ NullMarkedCollectionTargetBean nullMarkedCollectionTargetBean = new NullMarkedCollectionTargetBean();
+
+ nullMarkedCollectionTargetBean.setValues( source.getValues() );
+
+ return nullMarkedCollectionTargetBean;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapperImpl.java
new file mode 100644
index 0000000000..5336db031a
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyIterableMethodMapperImpl.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-05-14T23:18:22+0200",
+ comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)"
+)
+public class JSpecifyIterableMethodMapperImpl implements JSpecifyIterableMethodMapper {
+
+ @Override
+ public List mapAll(List sources) {
+
+ List list = new ArrayList<>( sources.size() );
+ for ( NullMarkedSourceBean nullMarkedSourceBean : sources ) {
+ list.add( map( nullMarkedSourceBean ) );
+ }
+
+ return list;
+ }
+
+ @Override
+ public NullMarkedTargetBean map(NullMarkedSourceBean source) {
+
+ NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean();
+
+ nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() );
+ nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() );
+
+ return nullMarkedTargetBean;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapperImpl.java
new file mode 100644
index 0000000000..2a74b75c6e
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyMapMethodMapperImpl.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-05-14T23:30:08+0200",
+ comments = "version: , compiler: Eclipse JDT (Batch) 3.20.0.v20191203-2131, environment: Java 25 (Eclipse Adoptium)"
+)
+public class JSpecifyMapMethodMapperImpl implements JSpecifyMapMethodMapper {
+
+ @Override
+ public Map mapAll(Map sources) {
+
+ Map map = new LinkedHashMap<>( Math.max( (int) ( sources.size() / .75f ) + 1, 16 ) );
+
+ for ( java.util.Map.Entry entry : sources.entrySet() ) {
+ String key = entry.getKey();
+ NullMarkedTargetBean value = map( entry.getValue() );
+ map.put( key, value );
+ }
+
+ return map;
+ }
+
+ @Override
+ public NullMarkedTargetBean map(NullMarkedSourceBean source) {
+
+ NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean();
+
+ nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() );
+ nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() );
+
+ return nullMarkedTargetBean;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapperImpl.java
new file mode 100644
index 0000000000..c1efd39d0d
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnBeanMapperImpl.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-05-14T23:41:40+0200",
+ comments = "version: , compiler: Eclipse JDT (Batch) 3.20.0.v20191203-2131, environment: Java 25 (Eclipse Adoptium)"
+)
+public class JSpecifyNonNullReturnBeanMapperImpl implements JSpecifyNonNullReturnBeanMapper {
+
+ @Override
+ public NullMarkedTargetBean map(JSpecifyNonNullReturnBeanSourceBean source) {
+
+ NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean();
+
+ if ( source != null ) {
+ nullMarkedTargetBean.setNonNullByDefault( source.getValue() );
+ }
+
+ return nullMarkedTargetBean;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapperImpl.java
new file mode 100644
index 0000000000..8c6de34e37
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnIterableMapperImpl.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-05-14T23:55:43+0200",
+ comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)"
+)
+public class JSpecifyNonNullReturnIterableMapperImpl implements JSpecifyNonNullReturnIterableMapper {
+
+ @Override
+ public List mapAll(List sources) {
+ if ( sources == null ) {
+ return new ArrayList<>();
+ }
+
+ List list = new ArrayList<>( sources.size() );
+ for ( NullMarkedSourceBean nullMarkedSourceBean : sources ) {
+ list.add( map( nullMarkedSourceBean ) );
+ }
+
+ return list;
+ }
+
+ @Override
+ public NullMarkedTargetBean map(NullMarkedSourceBean source) {
+
+ NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean();
+
+ nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() );
+ nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() );
+
+ return nullMarkedTargetBean;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapperImpl.java
new file mode 100644
index 0000000000..ffd45d3855
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnMapMapperImpl.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-05-15T00:03:33+0200",
+ comments = "version: , compiler: Eclipse JDT (Batch) 3.20.0.v20191203-2131, environment: Java 25 (Eclipse Adoptium)"
+)
+public class JSpecifyNonNullReturnMapMapperImpl implements JSpecifyNonNullReturnMapMapper {
+
+ @Override
+ public Map mapAll(Map sources) {
+ if ( sources == null ) {
+ return new LinkedHashMap<>();
+ }
+
+ Map map = new LinkedHashMap<>( Math.max( (int) ( sources.size() / .75f ) + 1, 16 ) );
+
+ for ( java.util.Map.Entry entry : sources.entrySet() ) {
+ String key = entry.getKey();
+ NullMarkedTargetBean value = map( entry.getValue() );
+ map.put( key, value );
+ }
+
+ return map;
+ }
+
+ @Override
+ public NullMarkedTargetBean map(NullMarkedSourceBean source) {
+
+ NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean();
+
+ nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() );
+ nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() );
+
+ return nullMarkedTargetBean;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapperImpl.java
new file mode 100644
index 0000000000..01c2981ab1
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnStreamMapperImpl.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.stream.Stream;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-05T14:55:49+0200",
+ comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)"
+)
+public class JSpecifyNonNullReturnStreamMapperImpl implements JSpecifyNonNullReturnStreamMapper {
+
+ @Override
+ public Stream mapAll(Stream sources) {
+ if ( sources == null ) {
+ return Stream.empty();
+ }
+
+ return sources.map( nullMarkedSourceBean -> map( nullMarkedSourceBean ) );
+ }
+
+ @Override
+ public NullMarkedTargetBean map(NullMarkedSourceBean source) {
+
+ NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean();
+
+ nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() );
+ nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() );
+
+ return nullMarkedTargetBean;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapperImpl.java
new file mode 100644
index 0000000000..70b8fbea1d
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNonNullReturnUpdateIterableMapperImpl.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.List;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-05T15:01:46+0200",
+ comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)"
+)
+public class JSpecifyNonNullReturnUpdateIterableMapperImpl implements JSpecifyNonNullReturnUpdateIterableMapper {
+
+ @Override
+ public List mapAll(List sources, List target) {
+ if ( sources == null ) {
+ return target;
+ }
+
+ target.clear();
+ for ( NullMarkedSourceBean nullMarkedSourceBean : sources ) {
+ target.add( map( nullMarkedSourceBean ) );
+ }
+
+ return target;
+ }
+
+ @Override
+ public NullMarkedTargetBean map(NullMarkedSourceBean source) {
+
+ NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean();
+
+ nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() );
+ nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() );
+
+ return nullMarkedTargetBean;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapperImpl.java
new file mode 100644
index 0000000000..2a6950f71d
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyNullableSourceIterableMapperImpl.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-05T14:55:49+0200",
+ comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)"
+)
+public class JSpecifyNullableSourceIterableMapperImpl implements JSpecifyNullableSourceIterableMapper {
+
+ @Override
+ public List mapAll(List sources) {
+ if ( sources == null ) {
+ return null;
+ }
+
+ List list = new ArrayList<>( sources.size() );
+ for ( NullMarkedSourceBean nullMarkedSourceBean : sources ) {
+ list.add( map( nullMarkedSourceBean ) );
+ }
+
+ return list;
+ }
+
+ @Override
+ public NullMarkedTargetBean map(NullMarkedSourceBean source) {
+
+ NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean();
+
+ nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() );
+ nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() );
+
+ return nullMarkedTargetBean;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapperImpl.java
new file mode 100644
index 0000000000..52313a0327
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyReturnNullOverrideIterableMapperImpl.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-05T14:55:49+0200",
+ comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)"
+)
+public class JSpecifyReturnNullOverrideIterableMapperImpl implements JSpecifyReturnNullOverrideIterableMapper {
+
+ @Override
+ public List mapAll(List sources) {
+ if ( sources == null ) {
+ return new ArrayList<>();
+ }
+
+ List list = new ArrayList<>( sources.size() );
+ for ( NullMarkedSourceBean nullMarkedSourceBean : sources ) {
+ list.add( map( nullMarkedSourceBean ) );
+ }
+
+ return list;
+ }
+
+ @Override
+ public NullMarkedTargetBean map(NullMarkedSourceBean source) {
+
+ NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean();
+
+ nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() );
+ nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() );
+
+ return nullMarkedTargetBean;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapperImpl.java
new file mode 100644
index 0000000000..4f276edc03
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/nullcheck/jspecify/JSpecifyStreamMethodMapperImpl.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright MapStruct Authors.
+ *
+ * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package org.mapstruct.ap.test.nullcheck.jspecify;
+
+import java.util.stream.Stream;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2026-06-05T14:54:43+0200",
+ comments = "version: , compiler: javac, environment: Java 25 (Eclipse Adoptium)"
+)
+public class JSpecifyStreamMethodMapperImpl implements JSpecifyStreamMethodMapper {
+
+ @Override
+ public Stream mapAll(Stream sources) {
+
+ return sources.map( nullMarkedSourceBean -> map( nullMarkedSourceBean ) );
+ }
+
+ @Override
+ public NullMarkedTargetBean map(NullMarkedSourceBean source) {
+
+ NullMarkedTargetBean nullMarkedTargetBean = new NullMarkedTargetBean();
+
+ nullMarkedTargetBean.setNonNullByDefault( source.getNonNullByDefault() );
+ nullMarkedTargetBean.setExplicitlyNullable( source.getExplicitlyNullable() );
+
+ return nullMarkedTargetBean;
+ }
+}