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

Activity

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions