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.
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:Actual behavior
For
Optional<Map<K, V>>andOptional<List<E>>source properties the presence check is omitted andOptional#getis called unconditionally, so mapping a source whoseOptionalis empty throwsNoSuchElementException.Generated mapper (note the scalar property in the same method is guarded):
Runtime, mapping a
Sourcewhose three properties are allOptional.empty():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 caseOptionalexists to express.Related to #3976, but that one concerns
get()vsorElseThrow(); here the presence check is missing altogether.Steps to reproduce the problem
Source.java:Target.java:SourceMapper.java:Demo.java:Compiled with plain
javac(no build tool, no other annotation processors):Also reproduces with
nullValueMapMappingStrategy = RETURN_NULLand withnullValueCheckStrategy = ALWAYS.MapStruct Version
1.7.0.Beta2, Java 25.0.4 (Eclipse Adoptium), javac.