Repository: mapstruct/mapstruct (2025-07-29T16:28:20Z)

Issue:
Unknown fields in `@Ignored` lead to an internal error
Add an unknown field like `"foo"` to the [ZooMapper](https://github.com/mapstruct/mapstruct/blob/6b6600c370cb50b2fbf40f56412238974575ae85/processor/src/test/java/org/mapstruct/ap/test/ignored/ZooMapper.java#L21) which uses the new `@Ignored` annotation introduced with #3788, then this results in an error:

```
CompilationResultDescriptor [compilationResult=FAILED, diagnostics=[DiagnosticDescriptor: ERROR ZooMapper.java:13 Internal error in the mapping processor: java.lang.NullPointerException: Cannot invoke "javax.lang.model.element.Element.equals(Object)" because "e" is null
at org.mapstruct.ap.internal.processor.MapperAnnotatedFormattingMessenger.methodInMapperClass(MapperAnnotatedFormattingMessenger.java:140)
at org.mapstruct.ap.internal.processor.MapperAnnotatedFormattingMessenger.determineDelegationElement(MapperAnnotatedFormattingMessenger.java:136)
at org.mapstruct.ap.internal.processor.MapperAnnotatedFormattingMessenger.printMessage(MapperAnnotatedFormattingMessenger.java:69)
at org.mapstruct.ap.internal.model.BeanMappingMethod$Builder.handleDefinedMapping(BeanMappingMethod.java:1310)
at org.mapstruct.ap.internal.model.BeanMappingMethod$Builder.handleDefinedMappings(BeanMappingMethod.java:1175)
at org.mapstruct.ap.internal.model.BeanMappingMethod$Builder.build(BeanMappingMethod.java:296)
at org.mapstruct.ap.internal.model.AbstractBaseBuilder.lambda$createForgedAssignment$1(AbstractBaseBuilder.java:92)
at org.mapstruct.ap.internal.model.AbstractBaseBuilder.getOrCreateForgedAssignment(AbstractBaseBuilder.java:108)
at org.mapstruct.ap.internal.model.AbstractBaseBuilder.createForgedAssignment(AbstractBaseBuilder.java:95)
at org.mapstruct.ap.internal.model.PropertyMapping$PropertyMappingBuilder.forgeMapping(PropertyMapping.java:832)
at org.mapstruct.ap.internal.model.PropertyMapping$PropertyMappingBuilder.forgeMapping(PropertyMapping.java:796)
at org.mapstruct.ap.internal.model.PropertyMapping$PropertyMappingBuilder.forge(PropertyMapping.java:323)
at org.mapstruct.ap.internal.model.PropertyMapping$PropertyMappingBuilder.build(PropertyMapping.java:270)
at org.mapstruct.ap.internal.model.BeanMappingMethod$Builder.applyPropertyNameBasedMapping(BeanMappingMethod.java:1624)
at org.mapstruct.ap.internal.model.BeanMappingMethod$Builder.applyPropertyNameBasedMapping(BeanMappingMethod.java:1586)
at org.mapstruct.ap.internal.model.BeanMappingMethod$Builder.build(BeanMappingMethod.java:314)
at org.mapstruct.ap.internal.processor.MapperCreationProcessor.getMappingMethods(MapperCreationProcessor.java:432)
at org.mapstruct.ap.internal.processor.MapperCreationProcessor.getMapper(MapperCreationProcessor.java:189)
at org.mapstruct.ap.internal.processor.MapperCreationProcessor.process(MapperCreationProcessor.java:143)
at org.mapstruct.ap.internal.processor.MapperCreationProcessor.process(MapperCreationProcessor.java:86)
at org.mapstruct.ap.MappingProcessor.process(MappingProcessor.java:373)
```

This should lead to a correct error instead of a stack trace.


Discussion:
Hi! I'd like to take on this issue.

From what I understand, the problem is that if a non-existent field name is provided in the `@Ignored` annotation, the annotation processor throws an internal `NullPointerException` instead of a clear, user-friendly compile-time error.

My proposed approach to fix this is as follows:

1.  First, I will write a new failing test case that reproduces the `NullPointerException` by using `@Ignored` with a property that doesn't exist in the source or target object.
2.  Based on the stack trace, I will locate the logic in `BeanMappingMethod$Builder` (or a related class) that resolves the properties listed in `@Ignored`.
3.  I will add a validation step to check if the property lookup was successful. If a property is not found (i.e., the result is `null`), I will use MapStruct's `Messager` utility to report a proper diagnostic error, such as "Unknown property 'foo' in @Ignored."
4.  Finally, I'll ensure the new test passes with this change and that all existing tests are unaffected.

As this would be my first contribution to MapStruct, I believe this is a great issue to get started with. Is anyone else currently working on this? If not, I would be happy to start working on a PR.
@thunderhook I've submitted a PR( #3908 ) that I believe resolves this issue. It includes a new test case and the corresponding fix.

I'd appreciate it if you could take a look when you have a moment. Let me know if there are any other changes or tests required.
Thank you!
@znight1020 Thanks!



Patch:
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java
index b5208d302b..c81fc51800 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java
@@ -1280,40 +1280,54 @@ else if ( inheritContext.isReversed() ) {
                             return false;
                         }
                     }
-                    Set<String> readAccessors = resultTypeToMap.getPropertyReadAccessors().keySet();
-                    String mostSimilarProperty = Strings.getMostSimilarWord( targetPropertyName, readAccessors );
 
                     Message msg;
                     String[] args;
 
-                    if ( targetRef.getPathProperties().isEmpty() ) {
-                        msg = Message.BEANMAPPING_UNKNOWN_PROPERTY_IN_RESULTTYPE;
+                    Element elementForMessage = mapping.getElement();
+                    if ( elementForMessage == null ) {
+                        elementForMessage = method.getExecutable();
+                    }
+
+                    if ( mapping.isIgnored() && mapping.getElement() == null ) {
+                        msg = Message.BEANMAPPING_UNKNOWN_PROPERTY_IN_IGNORED;
                         args = new String[] {
                             targetPropertyName,
-                            resultTypeToMap.describe(),
-                            mostSimilarProperty
+                            resultTypeToMap.describe()
                         };
                     }
                     else {
-                        List<String> pathProperties = new ArrayList<>( targetRef.getPathProperties() );
-                        pathProperties.add( mostSimilarProperty );
-                        msg = Message.BEANMAPPING_UNKNOWN_PROPERTY_IN_TYPE;
-                        args = new String[] {
-                            targetPropertyName,
-                            resultTypeToMap.describe(),
-                            mapping.getTargetName(),
-                            Strings.join( pathProperties, "." )
-                        };
+                        Set<String> readAccessors = resultTypeToMap.getPropertyReadAccessors().keySet();
+                        String mostSimilarProperty = Strings.getMostSimilarWord( targetPropertyName, readAccessors );
+
+                        if ( targetRef.getPathProperties().isEmpty() ) {
+                            msg = Message.BEANMAPPING_UNKNOWN_PROPERTY_IN_RESULTTYPE;
+                            args = new String[] {
+                                targetPropertyName,
+                                resultTypeToMap.describe(),
+                                mostSimilarProperty
+                            };
+                        }
+                        else {
+                            List<String> pathProperties = new ArrayList<>( targetRef.getPathProperties() );
+                            pathProperties.add( mostSimilarProperty );
+                            msg = Message.BEANMAPPING_UNKNOWN_PROPERTY_IN_TYPE;
+                            args = new String[] {
+                                targetPropertyName,
+                                resultTypeToMap.describe(),
+                                mapping.getTargetName(),
+                                Strings.join( pathProperties, "." )
+                            };
+                        }
                     }
 
-                    ctx.getMessager()
-                        .printMessage(
-                            mapping.getElement(),
-                            mapping.getMirror(),
-                            mapping.getTargetAnnotationValue(),
-                            msg,
-                            args
-                        );
+                    ctx.getMessager().printMessage(
+                        elementForMessage,
+                        mapping.getMirror(),
+                        mapping.getTargetAnnotationValue(),
+                        msg,
+                        args
+                    );
                     return true;
                 }
                 else if ( mapping.getInheritContext() != null && mapping.getInheritContext().isReversed() ) {
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java
index 883b3e3792..8534758285 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java
@@ -45,6 +45,7 @@ public enum Message {
     BEANMAPPING_CYCLE_BETWEEN_PROPERTIES( "Cycle(s) between properties given via dependsOn(): %s." ),
     BEANMAPPING_UNKNOWN_PROPERTY_IN_DEPENDS_ON( "\"%s\" is no property of the method return type." ),
     BEANMAPPING_IGNORE_BY_DEFAULT_WITH_MAPPING_TARGET_THIS( "Using @BeanMapping( ignoreByDefault = true ) with @Mapping( target = \".\", ... ) is not allowed. You'll need to explicitly ignore the target properties that should be ignored instead." ),
+    BEANMAPPING_UNKNOWN_PROPERTY_IN_IGNORED("No property named \"%s\" exists in @Ignored for target type \"%s\""),
 
     CONDITION_MISSING_APPLIES_TO_STRATEGY("'appliesTo' has to have at least one value in @Condition" ),
     CONDITION_SOURCE_PARAMETERS_INVALID_PARAMETER("Parameter \"%s\" cannot be used with the ConditionStrategy#SOURCE_PARAMETERS. Only source and @Context parameters are allowed for conditions applicable to source parameters." ),


Test patch:
diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3902/ErroneousIssue3902Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3902/ErroneousIssue3902Mapper.java
new file mode 100644
index 0000000000..07f94ce6fe
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3902/ErroneousIssue3902Mapper.java
@@ -0,0 +1,69 @@
+/*
+ * 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._3902;
+
+import org.mapstruct.Ignored;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * Mapper for testing bug #3902.
+ *
+ * @author znight1020
+ */
+@Mapper
+public interface ErroneousIssue3902Mapper {
+
+    ErroneousIssue3902Mapper INSTANCE = Mappers.getMapper( ErroneousIssue3902Mapper.class );
+
+    @Ignored(targets = {"name", "foo", "bar"})
+    ZooDto mapWithOneKnownAndMultipleUnknowns(Zoo source);
+
+    @Ignored(targets = {"name", "address", "foo"})
+    ZooDto mapWithMultipleKnownAndOneUnknown(Zoo source);
+
+    class Zoo {
+        private String name;
+        private String address;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public String getAddress() {
+            return address;
+        }
+
+        public void setAddress(String address) {
+            this.address = address;
+        }
+    }
+
+    class ZooDto {
+        private String name;
+        private String address;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public String getAddress() {
+            return address;
+        }
+
+        public void setAddress(String address) {
+            this.address = address;
+        }
+    }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_3902/Issue3902Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3902/Issue3902Test.java
new file mode 100644
index 0000000000..f1332bbdc6
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_3902/Issue3902Test.java
@@ -0,0 +1,57 @@
+/*
+ * 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._3902;
+
+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;
+
+/**
+ * Verifies that using an unknown property in {@code @Ignored} yields a proper
+ * compile error instead of an internal processor error.
+ *
+ * @author znight1020
+ */
+@WithClasses({ErroneousIssue3902Mapper.class})
+@IssueKey("3902")
+public class Issue3902Test {
+
+    @ProcessorTest
+    @ExpectedCompilationOutcome(
+            value = CompilationResult.FAILED,
+            diagnostics = {
+                    // Test case: mapWithOneKnownAndMultipleUnknowns
+                    @Diagnostic(
+                            type = ErroneousIssue3902Mapper.class,
+                            kind = javax.tools.Diagnostic.Kind.ERROR,
+                            line = 23,
+                            message = "No property named \"foo\" exists in @Ignored for target type " +
+                                    "\"ErroneousIssue3902Mapper.ZooDto\""
+                    ),
+                    @Diagnostic(
+                            type = ErroneousIssue3902Mapper.class,
+                            kind = javax.tools.Diagnostic.Kind.ERROR,
+                            line = 23,
+                            message = "No property named \"bar\" exists in @Ignored for target type " +
+                                    "\"ErroneousIssue3902Mapper.ZooDto\""
+                    ),
+
+                    // Test case: mapWithMultipleKnownAndOneUnknown
+                    @Diagnostic(
+                            type = ErroneousIssue3902Mapper.class,
+                            kind = javax.tools.Diagnostic.Kind.ERROR,
+                            line = 26,
+                            message = "No property named \"foo\" exists in @Ignored for target type " +
+                                    "\"ErroneousIssue3902Mapper.ZooDto\""
+                    )
+            }
+    )
+    public void shouldFailOnUnknownPropertiesInIgnored() {
+    }
+}