Skip to content

Be able to generate the nullable mapping from the NotNull mapping #4106

Description

@pkernevez

Use case

We have a lot of technical mapping like date, enum (domain <-> Dto), etc.
I would like to write only the not null version add have mapstruct generating the boiling code to deal with the Nullable version when needed.

Exa

Generated Code

Mapper definition:

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.mapstruct.AnnotateWith;
import org.mapstruct.Mapper;

import java.time.OffsetDateTime;
import java.time.ZonedDateTime;

import static tech.lightframe.shift.shared.TimeZoneConstants.UTC_ZONE_ID;

@Mapper
@AnnotateWith(NullMarked.class)
@NullMarked
public interface SimpleMapper {

    record SourceEntity(ZonedDateTime createdAt, @Nullable ZonedDateTime closedAt) {
    }

    record TargetDto(OffsetDateTime createdAt, @Nullable OffsetDateTime closedAt) {
    }

    // Only define the mapping once for non null
    default OffsetDateTime toOffsetTime(ZonedDateTime zt) {
        return zt.withZoneSameInstant(UTC_ZONE_ID).toOffsetDateTime();
    }

    /// The mapping method should be able to map it
    TargetDto toDto(SourceEntity sourceEntity);
}

What is generated now:

@NullMarked
@Component
public class SimpleMapperImpl implements SimpleMapper {

    @Override
    public SimpleMapper.TargetDto toDto(SimpleMapper.SourceEntity sourceEntity) {

        OffsetDateTime createdAt = null;
        OffsetDateTime closedAt = null;

        createdAt = toOffsetTime( sourceEntity.createdAt() );
        closedAt = toOffsetTime( sourceEntity.closedAt() ); <= Invalid code, it silently pass a null value to nonnull method

        SimpleMapper.TargetDto targetDto = new SimpleMapper.TargetDto( createdAt, closedAt );

        return targetDto;
    }
}

Expected:

@NullMarked
@Component
public class SimpleMapperImpl implements SimpleMapper {

    @Override
    public SimpleMapper.TargetDto toDto(SimpleMapper.SourceEntity sourceEntity) {

        OffsetDateTime createdAt = null;
        OffsetDateTime closedAt = null;

        createdAt = toOffsetTime( sourceEntity.createdAt() );
        if (sourceEntity.closedAt() != null){
            closedAt = toOffsetTime( sourceEntity.closedAt() );
        }
        SimpleMapper.TargetDto targetDto = new SimpleMapper.TargetDto( createdAt, closedAt );

        return targetDto;
    }
}

It's not possible to define 2 mapping methods like that:

    // Only define the mapping once for non null
    default OffsetDateTime toOffsetTime(ZonedDateTime zt) {
        return zt.withZoneSameInstant(UTC_ZONE_ID).toOffsetDateTime();
    }

    default @Nullable OffsetDateTime toOffsetTimeNull(@Nullable ZonedDateTime zt) {
        if (zt == null) {
            return null;
        }
        return zt.withZoneSameInstant(UTC_ZONE_ID).toOffsetDateTime();
    }

With this configuration MapStruct fails with the error:

Ambiguous mapping methods found for mapping property "ZonedDateTime createdAt" to OffsetDateTime: OffsetDateTime toOffsetTime(ZonedDateTime zt), OffsetDateTime toOffsetTimeNull(ZonedDateTime zt). See https://mapstruct.org/faq/#ambiguous for more info.

Possible workarounds

Use a qulifier with 2 different mapping.

MapStruct Version

1.7.0.Beta2

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions