Skip to content

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

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.

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