#4123·mapstruct

Optional source property that is itself null throws NullPointerException on the generated isPresent() check; NullValueCheckStrategy.ALWAYS is ignored

Author: veloCreated Sep 9, 2026Updated Sep 16, 2026

Expected behavior

When a source property is Optional<T>, the generated presence check should tolerate the Optional reference itself being null, e.g.:

java
if ( source.getName() != null && source.getName().isPresent() ) {
    name = source.getName().get();
}

or, at minimum, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS should produce that guard, since that is the documented knob for "always null-check the source before using it".

Actual behavior

The generated code calls isPresent() on the raw getter result, with no null check:

java
@Override
public Target toTarget(Source source) {
    if ( source == null ) {
        return null;
    }

    String name = null;

    if ( source.getName().isPresent() ) {
        name = source.getName().get();
    }

    Target target = new Target( name );

    return target;
}

If the Optional property is null (which is a plain reference like any other, and is what many object builders and deserializers leave it as), this throws:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.Optional.isPresent()" because the return value of "repro.Source.getName()" is null
	at repro.SourceMapperImpl.toTarget(SourceMapperImpl.java:20)
	at Demo.main(Demo.java:8)

The 1.7.0.Beta1 release notes say this is intentional: "Note that we are not doing any null checks for the optional properties. Instead, we do a check if the optional is present or not and map it." The problem is that this makes the generated mapper unsafe for any source object that can hold a null Optional, and there is no configuration to opt out of it.

NullValueCheckStrategy.ALWAYS has no effect here. With the identical model and

java
@Mapper(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
public interface SourceMapperAlways
{
    Target toTarget( Source source );
}

the generated body is byte-for-byte the same as with the default strategy — still a bare source.getName().isPresent(). That seems like the core issue: the setting that exists precisely to force source null checks is silently ignored for Optional properties.

This is not a hypothetical input. Two common cases where a null Optional source field arises:

  1. Generated builders. Lombok's @Builder leaves an unset Optional component as null, not Optional.empty() (verified below with Lombok 1.18.48).
  2. Jackson deserialization, where null (field absent from the JSON) and Optional.empty() (field explicitly null in the JSON) are deliberately distinguished — this exact scenario was raised in https://github.com/mapstruct/mapstruct/issues/674#issuecomment-4291090849 but there is no tracking issue for it, hence this report.

This is distinct from #4111, which is about the presence check being omitted entirely for Optional<Map> / Optional<Collection>; here the presence check is generated, but it is itself unguarded.

Workaround (works, but requires one @Condition method per mapper/type):

java
@Condition
default boolean isSet( Optional<String> value )
{
    return value != null && value.isPresent();
}

which produces if ( isSet( source.getName() ) ).

Steps to reproduce the problem

Minimal case, no Lombok, plain javac, no other annotation processors.

Source.java:

java
package repro;

import java.util.Optional;

public class Source
{
    private Optional<String> name;

    public Optional<String> getName()
    {
        return name;
    }

    public void setName( Optional<String> name )
    {
        this.name = name;
    }
}

Target.java:

java
package repro;

public record Target( String name )
{ }

SourceMapper.java:

java
package repro;

import org.mapstruct.Mapper;

@Mapper
public interface SourceMapper
{
    Target toTarget( Source source );
}

Demo.java:

java
import repro.*;

public class Demo
{
    public static void main( String[] args )
    {
        Source source = new Source();
        System.out.println( new SourceMapperImpl().toTarget( source ) );
    }
}
bash
javac -cp mapstruct-1.7.0.Beta2.jar \
      -processorpath mapstruct-processor-1.7.0.Beta2.jar:mapstruct-1.7.0.Beta2.jar \
      -s gen -d out src/repro/*.java src/Demo.java

Adding a second mapper with nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS over the same types generates an identical method body.

Lombok variant, showing that this is reachable without ever writing null by hand:

java
package repro;

import java.util.Optional;
import lombok.Builder;

@Builder
public record Source( String id, Optional<String> name )
{ }
java
Source source = Source.builder().id( "1" ).build();
System.out.println( "name field is: " + source.name() );   // => name field is: null
new SourceMapperImpl().toTarget( source );                  // => NPE
name field is: null
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.Optional.isPresent()" because the return value of "repro.Source.name()" is null
	at repro.SourceMapperImpl.toTarget(SourceMapperImpl.java:22)
	at Demo.main(Demo.java:9)

MapStruct Version

1.7.0.Beta2, Java 25.0.2 (Eclipse Adoptium), javac. Lombok 1.18.48 for the second variant.