Skip to content

Optional<Map> / Optional<Collection> source property generates unguarded Optional#get, throwing NoSuchElementException when empty #4111

Open

Description

@surecloud-jleite

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

  1. adityaanikam commented on Aug 14, 2026

    @adityaanikam

    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.

  2. Tcharl commented on Oct 3, 2026

    @Tcharl

    Not sure Optional is a good practice in itself

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions