Skip to content

Optional target does not use Lombok builder correctly #4046

@HubertBubert

Description

@HubertBubert

Expected behavior

Field Optional<Type> field, when Type is annotated with Lombok @Builder, is instantiated using a builder obtained through static factory method.

Actual behavior

Causes compilation error when the target type is not in the same package as mapper, as the mapping implementation tries to instantiate the builder using a constructor which has limited visibility.

Steps to reproduce the problem

Here is a simple test logic:

import lombok.Builder;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

public class Test {
    public static void main(String[] args) {
        final var source = new EmployeeSource(new PersonSource("John Doe"));
        Mappers.getMapper(LocalMapper.class).toTarget(source);
    }

    @Builder
    public record EmployeeTarget (
        Optional<PersonTarget> person
    ) {}

    @Builder
    public record PersonTarget (
        String name
    ) {}

    public record EmployeeSource (
        PersonSource person
    ) {}

    public record PersonSource (
        String name
    ) {}

    @Mapper
    public interface LocalMapper {
        EmployeeTarget toTarget(EmployeeSource source);
    }
}

This code compiles, but only because the mapper and PersonTarget are in the same package.
If you move the target types to a different package the compilation fails.

This is because the generated implementation is:

protected Optional<PersonTarget> personSourceToPersonTargetOptional(PersonSource personSource) {
    // ...
    PersonTarget.PersonTargetBuilder personTarget = new PersonTarget.PersonTargetBuilder();  // <-- incorrect
    // ...
}

But if you make the target field non-optional:

@Builder
public record EmployeeTarget (
    PersonTarget person
) {}

the generated implementation uses the builder correctly:

protected PersonTarget personSourceToPersonTarget(PersonSource personSource) {
    // ...
    PersonTarget.PersonTargetBuilder personTarget = PersonTarget.builder();    // <-- correct
    // ...
}

So this problem is specific to Optional mapping, so this super recent functionality added in the tested version.

Workaround
Disable builder for mapping logic:

@Mapper(builder = @org.mapstruct.Builder(disableBuilder = true))

You can still use builders in your logic, simply the generated implementation will omit it.

MapStruct Version

1.7.0.Beta1

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions