Skip to content

JSpecify: False positive error for nullable source with nullable-param non-null-return method #4086

Open

Description

@RaniAgus

Expected behavior

When a @Nullable source property is mapped to a @NonNull constructor parameter via a reused mapping method that accepts @Nullable (explicitly handles null) and returns a non-null value, the mapping should compile successfully. The reused method's parameter and return type annotations prove the mapping is safe - null is handled and the result is guaranteed non-null.

Given:

@Mapper
public interface Issue4086Mapper {
    @Mapping(target = "payload", source = "value", qualifiedByName = "mapValue")
    Target map(Source source);

    @Named("mapValue")
    default Target.Nested mapValue(@Nullable String value) {
        return value == null ? new Target.Nested( "default" ) : new Target.Nested( value );
    }
}

public class Source {
    private String value;
    public @Nullable String getValue() { return value; }
}

public class Target {
    private final Nested payload;
    public Target(@NonNull Nested payload) { this.payload = payload; }
}

Expected: compilation succeeds with:

public Target map(Source source) {
    Target.Nested payload = mapValue( source.getValue() );
    Target target = new Target( payload );
    return target;
}

Actual behavior

Compilation fails with:

error: Can't map potentially nullable source property "value" to @NonNull
constructor parameter "payload". Consider adding a defaultValue or defaultExpression.

The current check only inspects the source getter chain nullability, which reports NULLABLE. It does not consider that the assignment was resolved through a reused method whose parameter is @Nullable (no null guard needed before the call) and whose return type is @NonNull (the result is guaranteed non-null).

Steps to reproduce the problem

A @Nullable source property mapped to a @NonNull constructor parameter
via qualifiedByName referencing a method with @Nullable parameter and non-null return. Can be reproduced in this branch: main...RaniAgus:mapstruct:issues/4086

MapStruct Version

1.7.0.Beta2

Activity

  1. added this to the 1.7.0.RC1 milestone on Jul 5, 2026
  2. added 4 commits that reference this issue on Jul 24, 2026
  3. r0h1tb commented on Aug 1, 2026

    @r0h1tb

    I had a go at this and got as far as pinning the root cause, so posting it in case it's useful to whoever picks it up.

    The error is raised in PropertyMapping.java (PropertyMappingBuilder.build(), ~L286–303):

    if ( assignment != null
        && targetWriteAccessorType == AccessorType.PARAMETER
        && !hasDefaultValueOrDefaultExpression() ) {
        NullabilityResolver.Nullability sourceNullability = getSourceJSpecifyNullability();
        NullabilityResolver.Nullability targetNullability = ctx.getNullabilityResolver().getSetterNullability( ... );
        if ( sourceNullability != NON_NULL && targetNullability == NON_NULL ) {
            // PROPERTYMAPPING_NULLABLE_SOURCE_TO_NON_NULL_CONSTRUCTOR_PARAM
        }
    }

    The condition only looks at the source property and the target constructor parameter. It has assignment in scope but never consults it — so a value routed through a qualified method whose return type is non-null is treated identically to a value assigned directly. In the reported case mapValue(@Nullable String) -> Target.Nested makes the result provably non-null, and the diagnostic still fires.

    The obstacle to the obvious fix — "skip the error when the assignment's method returns non-null" — is that the return nullability isn't reachable from there. MethodReference takes a Method in its constructor but only retains method.getReturnType():

    protected MethodReference(Method method, MapperReference declaringMapper, Parameter providingParameter, ...) {
        this.returnType = method.getReturnType();
        // no Method / ExecutableElement retained
    }

    NullabilityResolver.getNullability(ExecutableElement, ...) already returns the return type nullability for an ExecutableElement, so the resolver side needs nothing new — the missing piece is that the ExecutableElement (or a precomputed nullability flag) isn't carried on MethodReference to the point where the check runs.

    So the fix seems to need one of:

    1. retain the Method on MethodReference, or
    2. compute the return Nullability at construction and store that flag,

    then gate the diagnostic on it. (2) is narrower but still touches a core model class on every mapping path, so it's worth a maintainer opinion on the preferred shape before someone writes it.

    Worth noting the same gap likely affects #4081 — both are the reused-method return-nullability path not being considered.

    I haven't opened a PR; flagging the analysis rather than guessing at the architecture.

  4. self-assigned this
    on Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions