Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Discussion options

I am trying to map a source value to a target that "walks" multiple instances. For example, say I am receiving a latitude/longitude from a source and wish to map it to the path myInstance.currentLocation.latitude. The currentLocation is an intermediate object that may already exist but will have to be instantiated if not.

So the java domain classes are:

class MyInstance{
   GeoLocation currentLocation;
}
abstract class GeoLocation{
   // getLatitude / setLatitude methods
}
class PointLocation extends GeoLocation{
}

Mapstruct maps this as follows (roughly; I am simplifying the actual code):

if ( myInstance.currentLocation == null ) {
   myInstance.currentLocation = new GeoLocation();
}
myInstance.currentLocation.setLatitude( sourcePojo.latitude );

The fatal problem is that it is trying to instantiate the abstract class GeoLocation instead of the correct class PointLocation.

I know how to solve this by writing my own custom code mapping method, but I wonder if there is a convenient way in mapstruct to control the instantiation of these "intermediate" instances? I cannot find anything in the manual.

You must be logged in to vote

Replies: 7 comments · 6 replies

Comment options

Can you please provide the mapper that have as well?

The problem here is that MapStruct has no way of knowing that MyInstance.currentLocation should be PointLocation, it only sees that it is GeoLocation.

You might be able to solve it by changing your mapper, but would need to see your existing mapping to be able to help more there

You must be logged in to vote
0 replies
Comment options

Yes, that is the problem. And a pretty general situation.

I'd use whatever mapper configuration solves the problem. But I currently use an interface for the Mapping and Mapstruct instantiation:

@Mapper(
	config = CommonMappingConfig.class,
	builder = @Builder(disableBuilder = true),
	uses = { CommonMapping.class, DateMapping.class, DimensionedUnitsMapping.class, EnumMapping.class })

public interface LocationMapping {

	LocationMapping INSTANCE = newMapper();

	static LocationMapping newMapper() {
		LocationMapping mapper = Mappers.getMapper(LocationMapping.class);
		return mapper;
	}
	
	@Mapping(target = "currentLocation.time", source = "createdAt", qualifiedBy = DateTimeGroup.class)
	@Mapping(target = "currentLocation.lat", source = "latitude")
	@Mapping(target = "currentLocation.lng", source = "longitude")
	
	void fromPojo(PojoLocation pojo, @MappingTarget ModelWithLocation model, @Context LocationMapping_Ctx ctx);
}
You must be logged in to vote
3 replies
@filiphr
Comment options

Thanks for sharing this. Seems like you are doing update mappings and we would try to set some values if the value was null. What you could try (not sure if it'll work) is to add a method like:

@Mapping(target = "currentLocation.lng", source = "pojo")
void fromPojo(PojoLocation pojo, @MappingTarget ModelWithLocation model, @Context LocationMapping_Ctx ctx);

@BeanMapping(resultType = PointLocation.class)
@Mapping(target = "time", source = "createdAt", qualifiedBy = DateTimeGroup.class)
@Mapping(target = "lat", source = "latitude")
@Mapping(target = "lng", source = "longitude")
GeoLocation mapToLocation(PojoLocation pojo, @Context LocationMapping_Ctx ctx);

This can perhaps avoid the

if ( myInstance.currentLocation == null ) {
   myInstance.currentLocation = new GeoLocation();
}

part of the generated code.

@RichMacDonald
Comment options

Eyeballing that, did you mean to write:

@Mapping(target = "currentLocation", source = "pojo")

Nice idea. And I can see it working for new instances. However, myInstance.currentLocation may not be null. And I'd need the ModelWithLocation instance to know which subclass of GeoLocation to instantiate. So the mapping method would have to be:

GeoLocation mapToLocation(PojoLocation pojo, ModelWithLocation model, @Context LocationMapping_Ctx ctx);

With handwritten code to check for new or existing GeopLocation instances. And I don't see how that works.

Luckily, my LocationMapping_Ctx implementation always stores the current pojo and model instance. And it also can provide all the mapping pairs the pojo and model instances., So I have a "trick" in place that maybe could make this work.

But this is quite a bit harder to follow. I'll keep this option in my backpocket. For now, I prefer the ObjectFactory approach as that is simpler.

By the way, the reason I use update mapping instead of newInstance mapping is that all my use cases are between networks of connected objects: A list of Pojos becomes a list of Models and vice-versa. We populate both primitive fields and "foreign key" fields ("id" fields to other instances). So all mappings are two-pass: Pass-one instantiates everything, then Pass-two populates all fields and "foreign keys".

Synching between pojo and model happens many times, we are always creating and/or updating (also deleting).

@filiphr
Comment options

Eyeballing that, did you mean to write:

@Mapping(target = "currentLocation", source = "pojo")

Yes indeed, that's what I meant. Made a typo.

Nice idea. And I can see it working for new instances.

Indeed you are right. It won't fully work for what you need. If the approach with the @ObjectFactory works then that's the best solution for now

Comment options

  1. Current workaround is writing a method in the LocationMapping interface to do it. Use @AfterMapping to write the conversion between pojo and model.
@AfterMapping
default void fromPojo_currentLocation(PojoLocation pojo, @MappingTarget ModelWithLocation model, @Context LocationMapping_Ctx ctx){
	if (pojo.createdAt == null && pojo.lat == null && pojo.lng == null){
		return;
	}
	GeoLocation loc = model.getCurrentLocation();
	if (loc == null){
		loc = new PointLocation();   //can use the subclass because I know the context of the code. Could also delegate to the ctx instance.
		model.setCurrentLocation(loc);
	}
	loc.time = pojo.createdAt;
	loc.lat = pojo.lat;
	loc.lng = pojo.lng;
}
You must be logged in to vote
0 replies
Comment options

  1. Writing a getCurrentPointLocationEnsured method on the myInstance. (Also have to write a corresponding setter to make mapstruct happy).
class ModelWithLocation{
   public GeoLocation getGeoLocationEnsured(){
		if (currentLocation == null){
		   currentLocation = newCurrentLocation(); //internal method. May not know the current context, though
		}
		return currentLocation;
	}
	///make the java bean specification happy with a matching setter
	public void setGeoLocationEnsured(GeoLocation loc){
	   setGeoLocation(loc);
	}
}

And the mapping becomes:

@Mapping(target = "currentLocationEnsured.time", source = "createdAt", qualifiedBy = DateTimeGroup.class)
@Mapping(target = "currentLocationEnsured.lat", source = "latitude")
@Mapping(target = "currentLocationEnsured.lat", source = "latitude")
@Mapping(target = "currentLocation.lng", source = "longitude")

Note that this will still create a compile error in LocationMapping Impl, but the error is in a branch that will never execute.

I could write a getPointLocationEnsured method to avoid the compile error.

You must be logged in to vote
0 replies
Comment options

  1. Write a BeforeMapping method that creates the Location.

Still have the compile error in the LocationMappingImpl class but it will never execute.

Disadvantage is that we have to instantiate a PointLocation even if all the pojo fields are null. If that is a real concern then add an AfterMapping method to check for nulls and remove the PointLocation if it has null field values.

	@BeforeMapping
	default void fromPojo_location(PojoLocation pojo, @MappingTarget ModelWithLocation model, @Context LocationMapping_Ctx ctx) {
		GeoLocation geo = model.getCurrentLocation();
		if (geo == null) {
			geo = ctx.newGeoLocation(pojo, model); //let the context decide which subclass to instantiate
			model.setCurrentLocation(geo);
		}
	}
You must be logged in to vote
0 replies
Comment options

  1. Brainstorming MapStruct future solutions:

Add a field to Mapping that handles the path population. Mapstruct calls this method instead of doing it itself

@Mapping(target = "currentLocation.time", source = "createdAt", qualifiedBy = DateTimeGroup.class, pathPopulateBy= CurrentLocationPopulateAnnot.class)
@Mapping(target = "currentLocation.lat", source = "latitude", pathPopulateBy= CurrentLocationPopulateAnnot.class)
@Mapping(target = "currentLocation.lng", source = "longitude", pathPopulateBy= CurrentLocationPopulateAnnot.class)


@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
@interface CurrentLocationPopulateAnnot {}

@CurrentLocationPopulateAnnot
default void currentLocationPopulate(PojoLocation pojo, @MappingTarget ModelWithLocation model, @Context LocationMapping_Ctx ctx){
	if (pojo.createdAt == null && pojo.lat == null && pojo.lng == null){
		//Aside: This is wishful thinking because even though I use a @Condition method to check for nulls in my standard mapping, will Mapstruct use it for each step along the path?
		return;
	}
	GeoLocation geo = model.getCurrentLocation();
	if (geo == null){
		geo = ctx.newGeoLocation(pojo, model); //let the context decide which subclass to instantiate
		model.setCurrentLocation(geo);
	}
}
You must be logged in to vote
2 replies
@filiphr
Comment options

//Aside: This is wishful thinking because even though I use a @condition method to check for nulls in my standard mapping, will Mapstruct use it for each step along the path?

Maybe you can try the new (from 1.6) @SourceParameterCondition (see https://mapstruct.org/news/2024-05-11-mapstruct-1_6_0_Beta2-is-out/#conditional-mapping-for-source-parameters and https://mapstruct.org/documentation/stable/reference/html/#conditional-mapping). MapStruct could call it and you do the null checks.

ctx.newGeoLocation(pojo, model); //let the context decide which subclass to instantiate

I haven't tried, but try adding that method with @ObjectFactory into your context. It might actually work 😉

@RichMacDonald
Comment options

Nice! Had not seen @SourceParameterCondition. I'll play with it to see how precisely I can apply it to this problem.

In the meantime, @ObjectFactory is exactly what I needed. Add this method to the LocationMapping interface:

@ObjectFactory
default GeoLocation createGeoLocation(@Context LocationMapping_Ctx ctx) {
   // Let the context decide which subclass to instantiate
   // Cannot add the PojoLocation or the ModelWithLocation instance to this method, however.
   // But luckily, my ctx instance keeps track of the current pojo and model instances so it does not need them.
   return ctx.newGeoLocation(); 
}

And the generated LocationMappingImpl changes to:

if ( model.getCurrentLocation() == null ) {
   model.setCurrentLocation( createGeoLocation( ctx ) );
}

This moves me forward. Thank you very much for your help, Filip. Great tool!

Comment options

  1. Brainstorming MapStruct future solutions:

Use a method convention. Add the prefix "new" to the name of the path and write a new method in the LocationMapper interface. MapStruct calls this instead of instantiating GeoLocation itself.

GeoLocation newCurrentLocation(PojoLocation pojo, @MappingTarget ModelWithLocation model, @Context LocationMapping_Ctx ctx){
   return ctx.newGeoLocation(pojo, model); //let the context decide which subclass to instantiate
}
You must be logged in to vote
1 reply
@filiphr
Comment options

See my previous comment about this with the @ObjectFactory

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
2 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.