Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MapStruct does not always use builders in target-object #1997

Closed
mfriess2 opened this issue Jan 6, 2020 · 16 comments · Fixed by #2733
Closed

MapStruct does not always use builders in target-object #1997

mfriess2 opened this issue Jan 6, 2020 · 16 comments · Fixed by #2733
Labels
Milestone

Comments

@mfriess2
Copy link

mfriess2 commented Jan 6, 2020

Hi,

we try to use MapStruct with Lombok builders but MapStruct does not always seem to recognize and use the builders. Our particular problematic case is when the target object has a field which only has a builder.

Used MapStruct version: 1.3.1.FINAL
Used Lombok version: 1.18.10

It would be very helpful for us if you could let us know if this is considered a bug or not.
Looking forward to your feedback!

Here is an example:

Source POJO:

package example.model.source;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class Car {
	private String model;
}

Target POJOs:

package example.model.target;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class CarInsurance {
	private CarDetail carDetail;   // this is the problematic case
}

@Data
@Builder
public class CarDetail {
	private String model;
}

Mapper:

package example.mapper;

import example.model.source.Car;
import example.model.target.CarInsurance;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;

@Mapper
public interface CarInsuranceMapper {
	@Mapping(source = "model", target = "carDetail.model")
	void map(Car source, @MappingTarget CarInsurance target);
}

MapStruct generates this mapper:

package example.mapper;

import example.model.source.Car;
import example.model.target.CarDetail;
import example.model.target.CarInsurance;
import javax.annotation.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor"
)
@Component
public class CarInsuranceMapperImpl implements CarInsuranceMapper {

    @Override
    public void map(Car source, CarInsurance target) {
        if ( source == null ) {
            return;
        }

        if ( target.getCarDetail() == null ) {
            target.setCarDetail( new CarDetail() );  // THIS CREATES A COMPILE ERROR
        }
        carToCarDetail( source, target.getCarDetail() );
    }

    protected void carToCarDetail(Car car, CarDetail mappingTarget) {
        if ( car == null ) {
            return;
        }

        mappingTarget.setModel( car.getModel() );
    }
}

And the compiler fails with:

CarInsuranceMapperImpl.java:22: error: constructor CarDetail in class CarDetail cannot be applied to given types;
            target.setCarDetail( new CarDetail() );

It would work if MapStruct would generate:

if ( target.getCarDetail() == null ) {
    target.setCarDetail( CarDetail.builder().build() );
}

The annotation processor ordering in the Gradle build:

	implementation 'org.mapstruct:mapstruct'
	implementation 'org.projectlombok:lombok'
	annotationProcessor 'org.mapstruct:mapstruct-processor'
	annotationProcessor 'org.projectlombok:lombok'
@filiphr
Copy link
Member

filiphr commented Jan 15, 2020

@mfriess2 I think that this is the same as #1581 and is caused by projectlombok/lombok#1538.

@filiphr filiphr closed this as completed Jan 15, 2020
@mfriess2
Copy link
Author

Thanks for taking the time to answer but I'm also a bit sad that you closed this issue so fast.

1 ) I am using the proposed solution from #1581 : lombok is after mapstruct (written in the gradle file; I think at execution time it is run reversed: first lombok then mapstruct)

To be double-sure, I tried out:

annotationProcessor 'org.mapstruct:mapstruct-processor', 'org.projectlombok:lombok'

similar to your example https://github.com/mapstruct/mapstruct-examples/blob/master/mapstruct-lombok/build.gradle (but I am using the newer "annotationProcessor" instead of "apt")

2 ) but most important: I de-lomboked the class "CarDetail" just to take Lombok out of the equation and the problem stays the same: the builder is not recognized/used by mapstruct and an uncompilable Mapper is generated

Here is the non-lombok class which provokes the same error:

package example.model.to;

public class CarDetail {
	private String model;

	private CarDetail(String model) {
		this.model = model;
	}

	public static CarDetailBuilder builder() {
		return new CarDetailBuilder();
	}

	public String getModel() {
		return this.model;
	}

	public void setModel(String model) {
		this.model = model;
	}

	public static class CarDetailBuilder {
		private String model;

		CarDetailBuilder() {
		}

		public CarDetail.CarDetailBuilder model(String model) {
			this.model = model;
			return this;
		}

		public CarDetail build() {
			return new CarDetail(model);
		}

		public String toString() {
			return "CarDetail.CarDetailBuilder(model=" + this.model + ")";
		}
	}
}

@mfriess2 mfriess2 changed the title MapStruct does not use Lombok-builder in target-object MapStruct does not always use builders in target-object Jan 16, 2020
@filiphr filiphr reopened this Jan 16, 2020
@filiphr
Copy link
Member

filiphr commented Jan 16, 2020

Sorry I didn't see that you are using update mapping. In this case we can't use the builder and we would use the setters only.

I understand the problem now. Can you try with 1.4.0-SNAPSHOT? I think that there was a similar problem and we have fixed it on master.

@mfriess2
Copy link
Author

mfriess2 commented Jan 16, 2020

Thanks & sure, I will build it and let you know soon.

@mfriess2
Copy link
Author

I tried it out with 076f3ba and unfortunately it did not work.
I uploaded the full example to: https://github.com/mfriess2/mapstruct-issue-1997

@filiphr filiphr added the bug label Jan 25, 2020
@filiphr filiphr added this to the 1.4.0 milestone Jan 25, 2020
@drenda
Copy link

drenda commented Apr 30, 2020

@filiphr Could my question here be related to this issue? Any possible workaround? Thanks

@filiphr filiphr modified the milestones: 1.4.0.Beta1, 1.4.0 May 30, 2020
@filiphr filiphr modified the milestones: 1.4.0, 1.5.0 Sep 19, 2020
@cedricwalter
Copy link

I can confirm the issue is still in version 1.4.1.Final

Any workaround?

@aalnaw2s
Copy link

aalnaw2s commented Nov 7, 2020

I am also confirming the same bug with the last version 1.4.1.Final.
the proposed solutions above are not working.

@thunderhook
Copy link
Contributor

@filiphr I can confirm that the problem starts appearing with lombok 1.18.14.

To reproduce this, adapt the mapstruct-lombok module in mapstruct-examples the following way:
Add @Builder on class Target and build the module. You will see that the mapper uses the builder.

Now, set <org.projectlombok.version> to 1.18.14 and rebuild the module. I have see randomly two issues

  • java: No property named "test" exists in source parameter(s). Did you mean "null"? ... that alone may be a symptom that mapstruct can't read the source property from a @Data annotated class
  • the mapper tries to use a no-arg-constructor and does not use the builder

I hope this helps investigating the underlying problem.

BTW: lombok-mapstruct-binding did not solve the problem, neither as dependency, nor as annotation processor path.

@filiphr
Copy link
Member

filiphr commented Nov 20, 2020

@thunderhook is the problem there with 1.18.16? According to the Lombok changelog 1.18.14 is broken and shouldn't be used

@thunderhook
Copy link
Contributor

Ah sorry, yes, this is reproducable with 1.18.16 as well.

@filiphr
Copy link
Member

filiphr commented Nov 21, 2020

@thunderhook are you by any chance using Java 8 to do the compilation?

lombok-mapstruct-binding does not work on Java 8 have a look at projectlombok/lombok#2616. We have an open PR (mapstruct/mapstruct-examples#111) to update our examples to use the latest Lombok and the CI is green for Java 9+

@thunderhook
Copy link
Contributor

thunderhook commented Nov 21, 2020

@thunderhook are you by any chance using Java 8 to do the compilation?

lombok-mapstruct-binding does not work on Java 8 have a look at rzwitserloot/lombok#2616. We have an open PR (mapstruct/mapstruct-examples#111) to update our examples to use the latest Lombok and the CI is green for Java 9+

Thanks for clarification. Yes I am using Java 8. You are right, choosing a higher Java version works at least with maven.

Changing to 9 (or 11, or 15, that's what i've tried) also shows java: No property named "test" exists in source parameter(s). Did you mean "null"? but only in IntelliJ.

From my own test-project using Java 15 this is also reproducable (that was easier than adapting the mapstruct-examples project).

with something like

@Builder
@Value
public class CustomerDTO {

    int customerNumber;
    String name;
}

IntelliJ generated mapper:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2020-11-21T17:47:56+0100",
    comments = "version: 1.4.1.Final, compiler: javac, environment: Java 15.0.1 (Oracle Corporation)"
)
public class CustomerMapperImpl implements CustomerMapper {

    @Override
    public CustomerDTO map(CustomerEntity source) {
        if ( source == null ) {
            return null;
        }

        CustomerDTO customerDTO = new CustomerDTO();
        // <- compile error java: constructor CustomerDTO in class bug.mapstruct.CustomerDTO cannot be applied to given types;
        //    required: int,java.lang.String
        //    found:    no arguments

        return customerDTO;
    }
}

Maven generated mapper:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2020-11-21T17:51:38+0100",
    comments = "version: 1.4.1.Final, compiler: javac, environment: Java 15.0.1 (Oracle Corporation)"
)
public class CustomerMapperImpl implements CustomerMapper {

    @Override
    public CustomerDTO map(CustomerEntity source) {
        if ( source == null ) {
            return null;
        }

        CustomerDTOBuilder customerDTO = CustomerDTO.builder();

        customerDTO.customerNumber( (int) source.getCustomerNumber() );
        customerDTO.name( source.getName() );

        return customerDTO.build();
    }
}

If I can provide any more info (mvn/idea log), or if this should be an own ticket, please let me know!

Edit: Ahh, damn! I wrote the annotation processor configuration wrong. Sorry. It is working now as intended! Thanks!

@metao1
Copy link

metao1 commented Apr 2, 2021

I still have this problem. I want to force map construct to use the builder method instead.

Model:

@Value
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Student {
    String name;
    int id;
}

Mapper Class:

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.WARN)
public interface DTOMapper {
    Student map(Student student);
}
```
```

 error: LogConfigDTO does not have an accessible constructor.
    LogConfigDTO mapLogConfigToDTO(ConfigModel configModel);
                 ^
```

@filiphr
Copy link
Member

filiphr commented Apr 5, 2021

@metao1 seems like you have cross posted this here and in the Lombok issue tracker projectlombok/lombok#2804. MapStruct by default uses builders (if it can see them), see projectlombok/lombok#1538 for more information

@FARHANE
Copy link

FARHANE commented May 26, 2021

Same problem
java 15
lombok 1.18.20
mapstruct 1.4.2.Final

 <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>${java.version}</source>
          <target>${java.version}</target>
          <annotationProcessorPaths>
            <path>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok-mapstruct-binding</artifactId>
              <version>${lombok-mapstruct-binding.version}</version>
            </path>
            <path>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
              <version>${lombok.version}</version>
            </path>
            <path>
              <groupId>org.mapstruct</groupId>
              <artifactId>mapstruct-processor</artifactId>
              <version>${org.mapstruct.version}</version>
            </path>
          </annotationProcessorPaths>
        </configuration>
      </plugin>
    </plugins>
  </build>
@Value
@Builder(toBuilder = true)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Jacksonized
public class MyClass implements MyInterface{

}
MyClass  does not have an accessible constructor.

@filiphr filiphr modified the milestones: 1.5.0.Beta1, 1.5.0 Jun 13, 2021
@filiphr filiphr modified the milestones: 1.5.0.Beta2, 1.5.0 Oct 24, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants