Repository navigation
Optional<Map> / Optional<Collection> source property generates unguarded Optional#get, throwing NoSuchElementException when empty #4111
Description
Expected behavior
When a source property is Optional<T>, the generated mapper should read the value only when it is present — as it already does for scalar types:
if ( source.getName().isPresent() ) {
name = source.getName().get();
}Actual behavior
For Optional<Map<K, V>> and Optional<List<E>> source properties the presence check is omitted and Optional#get is called unconditionally, so mapping a source whose Optional is empty throws NoSuchElementException.
Generated mapper (note the scalar property in the same method is guarded):
@Override
public Target toTarget(Source source) {
if ( source == null ) {
return null;
}
String name = null;
Map<String, String> attributes = null;
List<String> tags = null;
if ( source.getName().isPresent() ) {
name = source.getName().get();
}
attributes = source.getAttributes().get(); // no presence check
tags = source.getTags().get(); // no presence check
Target target = new Target( name, attributes, tags );
return target;
}Runtime, mapping a Source whose three properties are all Optional.empty():
Exception in thread "main" java.util.NoSuchElementException: No value present
at java.base/java.util.Optional.get(Optional.java:143)
at repro.SourceMapperImpl.toTarget(SourceMapperImpl.java:27)
at Demo.main(Demo.java:9)
The inconsistency between the scalar and the map/collection properties in the same generated method suggests the presence check is lost on the branch that handles map and collection types.
This makes Optional-returning getters unusable in practice for map and collection properties, since an absent value is exactly the case Optional exists to express.
Related to #3976, but that one concerns get() vs orElseThrow(); here the presence check is missing altogether.
Steps to reproduce the problem
Source.java:
package repro;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class Source
{
private final Optional<String> name;
private final Optional<Map<String, String>> attributes;
private final Optional<List<String>> tags;
public Source( Optional<String> name, Optional<Map<String, String>> attributes, Optional<List<String>> tags )
{
this.name = name;
this.attributes = attributes;
this.tags = tags;
}
public Optional<String> getName()
{
return name;
}
public Optional<Map<String, String>> getAttributes()
{
return attributes;
}
public Optional<List<String>> getTags()
{
return tags;
}
}Target.java:
package repro;
import java.util.List;
import java.util.Map;
public record Target( String name, Map<String, String> attributes, List<String> tags )
{ }SourceMapper.java:
package repro;
import org.mapstruct.Mapper;
@Mapper
public interface SourceMapper
{
Target toTarget( Source source );
}Demo.java:
import java.util.Optional;
import repro.*;
public class Demo
{
public static void main( String[] args )
{
Source source = new Source( Optional.empty(), Optional.empty(), Optional.empty() );
System.out.println( new SourceMapperImpl().toTarget( source ) );
}
}Compiled with plain javac (no build tool, no other annotation processors):
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
Also reproduces with nullValueMapMappingStrategy = RETURN_NULL and with nullValueCheckStrategy = ALWAYS.
MapStruct Version
1.7.0.Beta2, Java 25.0.4 (Eclipse Adoptium), javac.
Activity
adityaanikam commentedon Aug 14, 2026
Traced this to the exact template divergence. PropertyMapping.assignToPlainViaSetter wraps scalar assignments in SetterWrapper, which renders through the handleSourceReferenceNullCheck macro in CommonMacros.ftl, that macro explicitly checks sourceType.optionalType and emits isPresent() before the nested get(). The Map/Collection path goes through a completely different macro, handleLocalVarNullCheck, used by SetterWrapperForCollectionsAndMapsWithNullCheck and its siblings, which assigns the result of the whole expression, get() included, to a local var first and only null checks afterward, no Optional aware branch at all.
Given nullValueMapMappingStrategy and nullValueCheckStrategy both interact with this same code path, and your own testing shows the bug persists under both settings, I'd want to understand whether the intended fix is adding the same sourceType.optionalType branch to handleLocalVarNullCheck directly, or whether Optional unwrapping for collections should go through handleSourceReferenceNullCheck instead so both paths share one Optional aware macro. Happy to look into either once there's a preference.
Tcharl commentedon Oct 3, 2026
Not sure Optional is a good practice in itself