Collections no longer copied if marked non-null
Expected behavior
When copying a non-null (JSpecify) collection property not only the null-check is skipped (correctly) but also the copy of the collection is not performed. This means that if the source object had an immutable collection, now so does the destination object. Previously, the destination was always mutable.
I'm not sure if this is an intended change of behaviour or a defect. If it's intended, it contradicts section 5.2 of the documentation which says:
If source and target attribute have the same type, the value will be simply copied direct from source to target. If the attribute is a collection (e.g. a List) a copy of the collection will be set into the target attribute.
Actual behavior
Assuming we have a class Bar:
@NullMarked
class Bar {
private List<Foo> foos;
...In previous versions, a List copy (where source and destination are both Bars) would use this code:
List<Foo> list = source.getFoos();
if ( list != null ) {
destination.setFoos( new ArrayList<Foo>( list ) );
}However, if getFoos() is marked as non-null, in 1.7.0.Beta2 we now get:
destination.setFoos( source.getFoos() );This causes trouble for us because we actually use two MapStruct-generated methods as follows:
Bar copy(Bar source); // this used to leave `foos` mutable on the copied result
Bar patch(BarForm form, @MappingTarget Bar target); // this uses `clear()` and `addAll()` to modify `foos`The workaround is either to add an explicit mapping to copy to force the collection-copying as per the docs:
@Mapping(target = "foos", expression = "java(new ArrayList<>(source.getFoos()))")or to change the collection behaviour for the entire mapper using collectionMappingStrategy = CollectionMappingStrategy.TARGET_IMMUTABLE to stop it trying to clear() an immutable collection.
Steps to reproduce the problem
package mapstruct;
import java.util.List;
import org.jspecify.annotations.NullMarked;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
public class BarMapperTest {
public static void main(String[] args) {
Bar bar = new Bar(); // immutable foos
BarMapper mapper = Mappers.getMapper(BarMapper.class);
Bar patchTarget = mapper.copy(bar);
BarForm form = new BarForm(List.of(new Foo()));
mapper.patch(form, patchTarget);
System.out.println("Patched foos size: " + patchTarget.getFoos().size()); // Should print 1
}
}
@Mapper
interface BarMapper {
Bar copy(Bar source);
Bar patch(BarForm form, @MappingTarget Bar target);
}
@NullMarked
class Bar {
List<Foo> foos = List.of();
public List<Foo> getFoos() {
return foos;
}
public void setFoos(List<Foo> foos) {
this.foos = foos;
}
}
class Foo {}
class BarForm {
List<Foo> foos;
public BarForm(List<Foo> foos) {
this.foos = foos;
}
public List<Foo> getFoos() {
return foos;
}
}This blows up with:
Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:159)
at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.clear(ImmutableCollections.java:166)
at mapstruct.BarMapperImpl.patch(BarMapperImpl.java:38)
at mapstruct.BarMapperTest.main(BarMapperTest.java:18)In version 1.6.3 it completes successfully, printing Patched foos size: 1
MapStruct Version
1.7.0.Beta2
Source: mapstruct/mapstruct