#4118·mapstruct

Collections no longer copied if marked non-null

Author: gbodenSolumCreated Aug 25, 2026Updated Aug 27, 2026
LabelsbugJSpecify

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:

java
@NullMarked
class Bar {
  private List<Foo> foos;
...

In previous versions, a List copy (where source and destination are both Bars) would use this code:

java
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:

java
destination.setFoos( source.getFoos() );

This causes trouble for us because we actually use two MapStruct-generated methods as follows:

java
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:

java
@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

java
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