11

Today I started using MapStruct to create my Model to DTO converters for my project and i was wondering if it handled cyclic references automatically but it turned out it doesn't.

This is the converter i made to test it:

package it.cdc.snp.services.rest.giudizio;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
import org.springframework.stereotype.Component;

import it.cdc.snp.dto.entita.Avvisinotifica;
import it.cdc.snp.dto.entita.Corrispondenza;
import it.cdc.snp.model.notifica.AvvisoDiNotificaModel;
import it.cdc.snp.model.notifica.NotificaModel;
import it.cdc.snp.model.procedimento.ProcedimentoModel;

@Component
@Mapper(componentModel="spring")
public interface NotificaMapper {

    NotificaMapper INSTANCE = Mappers.getMapper( NotificaMapper.class );

    @Mappings({
        @Mapping(source = "avvisinotificas", target = "avvisinotificas"),
    })
    NotificaModel<ProcedimentoModel> corrispondenzaToNotificaModel(Corrispondenza notifica);

    @Mappings({
        @Mapping(source = "corrispondenza", target = "notifica"),
    })
    AvvisoDiNotificaModel avvisinotificaToAvvisoDiNotificaModel(Avvisinotifica avvisinotifica);


}

This is the test:

        Notifica sourceObject1 = new Notifica();
        sourceObject1.setId(new Long(1));
        Avvisinotifica sourceObject2 = new Avvisinotifica();
        sourceObject2.setId(new Long(11));
        List<Avvisinotifica> tests= new ArrayList<>();
        tests.add(sourceObject2);
        sourceObject1.setAvvisinotificas(tests);
        sourceObject2.setCorrispondenza(sourceObject1);

        NotificaModel destObject1 = new NotificaModel<>();
        Avvisinotifica destObject2 = new Avvisinotifica();

        NotificaModel converted = mapper.corrispondenzaToNotificaModel(sourceObject1);

Notifica, Avvisinotifica and their respective models are simple POJOs with setters and getters so i don't think it's needed to post the code (Notifica extends Corrispondenza, if you were wondering)

this code gets into an infinite cycle, nothing very surprising here (though i hoped it'd handle these situations). And while i think i can find an elegant way to manually handle it (i was thinking about using methods with @MappingTarget to insert the Referenced objects ) what i was wondering is if there's some way to tell MapStruct how to automatically handle cyclic references.

valepu
  • 3,136
  • 7
  • 36
  • 67

5 Answers5

23

Notifica and Avvisinotifica are not helping me understand your models. Thus lets say you have the above Child and Father models,

public class Child {
    private int id;
    private Father father;
    // Empty constructor and getter/setter methods omitted.
}

public class Father {
    private int x;
    private List<Child> children;
    // Empty constructor and getter/setter methods omitted.
}

public class ChildDto {
    private int id;
    private FatherDto father;
    // Empty constructor and getter/setter methods omitted.
}

public class FatherDto {
    private int id;
    private List<ChildDto> children;
    // Empty constructor and getter/setter methods omitted.
}  

You should create a Mapper like this,

@Mapper
public abstract class ChildMapper {

    @AfterMapping
    protected void ignoreFathersChildren(Child child, @MappingTarget ChildDto childDto) {
        childDto.getFather().setChildren(null);
    }

    public abstract ChildDto myMethod(Child child);
}

Initial Version of Mapstuct

It is better to follow the next ways. This solution assumes that the ChildDto::father property is of type Father, not FatherDto, which is not a correct data architecture.
The @AfterMapping annotation means that the method will be imported inside the generated source, after the mapping of the properties. Thus, the Mapper implementation will be like this,

@Component
public class ChildMapperImpl extends ChildMapper {

    @Override
    public ChildDto myMethod(Child child) {
        if ( child == null ) {
            return null;
        }

        ChildDto childDto = new ChildDto();

        childDto.setId( child.getId() );
        childDto.setFather( child.getFather() );

        ignoreFathersChildren( child, childDto );

        return childDto;
    }
}

In this implementation the child has the parent set. This means that a cycle reference exists, but using the ignoreFathersChildren(child, childDto) method we remove the reference (we set it as null).

Update 1

Using the mapstruct version 1.2.0.Final you can do it better,

@Mapper
public interface ChildMapper {

    @Mappings({
//         @Mapping(target = "father", expression = "java(null)"),
         @Mapping(target = "father", qualifiedByName = "fatherToFatherDto")})
    ChildDto childToChildDto(Child child);

    @Named("fatherToFatherDto")
    @Mappings({
         @Mapping(target = "children", expression = "java(null)")})
    FatherDto fatherToFatherDto(Father father);
}

Update 2

Using the mapstruct version 1.4.2.Final you can do it even better,

@Named("FatherMapper")
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface FatherMapper {

    @Named("toDto")
    @Mappings
    FatherDto toDto(Father father);

    @Named("toDtoWithoutChildren")
    @Mappings({
         @Mapping(target = "children", expression = "java(null)")})
    FatherDto toDtoWithoutChildren(Father father);
}

@Named("ChildMapper")
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE, uses = {FatherMapper.class})
public interface ChildMapper {

    @Named("toDto")
    @Mappings({
         @Mapping(target = "father", qualifiedByName = {"FatherMapper", "toDtoWithoutChildren"})})
    ChildDto toDto(Child child);

    @Named("toDtoWithoutFather")
    @Mappings({
         @Mapping(target = "father", expression = "java(null)")})
    ChildDto toDtoWithoutFather(Child child);
}
aioobe
  • 413,195
  • 112
  • 811
  • 826
Georgios Syngouroglou
  • 18,813
  • 9
  • 90
  • 92
  • 6
    An alternative possible as of MapStruct 1.2.0.Beta1 (released yesterday) would be to use a context parameter which keeps track of objects already mapped. You can find a complete example showing how to do this [here](https://github.com/mapstruct/mapstruct-examples/tree/master/mapstruct-mapping-with-cycles/src/main/java/org/mapstruct/example). – Gunnar Feb 21 '17 at 22:31
  • 1
    And what do I do with that Context thing? How do I instantiate it, where do I pass it when invoking the mapper method? – Ognjen Mišić Feb 05 '20 at 09:29
  • instead of using `expression="java(null)"`, you can use `ignore = true` – DependencyHell Dec 21 '20 at 09:58
  • 2
    Shouldn't the `FatherDto` have a `List` instead of a `List`? – axiopisty Sep 09 '21 at 03:44
  • childDto.setFather expects a FatherDto, NOT a Father – Wesley De Keirsmaeker Sep 21 '21 at 12:12
  • I think it is lacking the Mappings at FatherMapper at Update 2. – cviniciusm May 26 '22 at 20:26
  • it solves the issue at data access level and at json level. Unfortunately, this strategy is some cumbersome, for example the use case of Father -> Child -> Pet. – cviniciusm May 26 '22 at 20:31
8

At least in mapstruct 1.3 you can use the following:

The solution is widely inspired by https://github.com/mapstruct/mapstruct-examples/tree/master/mapstruct-mapping-with-cycles/src/main/java/org/mapstruct/example/mapper

Define a Context class (widely inspired by https://github.com/mapstruct/mapstruct-examples/blob/master/mapstruct-mapping-with-cycles/src/main/java/org/mapstruct/example/mapper/CycleAvoidingMappingContext.java ):

/**
 * An implementation to track cycles in graphs to be used as {@link Context} parameter.
 *
 */
public class CycleAvoidingMappingContext {
    private Map<Object, Object> knownInstances = new IdentityHashMap<Object, Object>();

    /**
     * Gets an instance out of this context if it is already mapped.
     * 
     * @param source
     *        given source
     * @param targetType
     *        given target type.
     * @return Returns the resulting type.
     */
    @BeforeMapping
    public <T> T getMappedInstance(Object source, @TargetType Class<T> targetType) {
        return targetType.cast(knownInstances.get(source));
    }

    /**
     * Puts an instance into the cache, so that it can be remembered to avoid endless mapping.
     * 
     * @param source
     *        given source
     * @param target
     *        given target
     */
    @BeforeMapping
    public void storeMappedInstance(Object source, @MappingTarget Object target) {
        knownInstances.put( source, target );
    }
}

In each mapper, which maps classes with cyclic references, add this org.mapstruct.Context:

/**
 * Mapper. Automatically implemented by mapstruct.
 * 
 */
@Mapper
public interface SomeObjWithCyclesMapper {

    /**
     * instance.
     */
    SomeObjWithCyclesMapper INSTANCE = Mappers.getMapper(SomeObjWithCyclesMapper.class);

    /**
     * Mapper method to map entity to domain. Automatically implemented by mapstruct.
     * 
     * @param entity
     *        given entity.
     * @param context
     *        context to avoid cycles.
     * @return Returns the domain object.
     */
    SomeObjWithCycles entityToDomain(SomeObjWithCyclesEntity entity, @Context CycleAvoidingMappingContext context);

    /**
     * Mapper method to map domain object to entity. Automatically implemented by mapstruct.
     * 
     * @param domain
     *        given domain object.
     * @param context
     *        context to avoid cycles.
     * @return Returns the entity.
     */
    SomeObjWithCyclesEntity domainToEntity(SomeObjWithCycles domain, @Context CycleAvoidingMappingContext context);
    
}

Usage (added 2021-09-21):

Then you can call the mapper method with:

SomeObjWithCyclesMapper.INSTANCE.domainToEntity(objWithCycles, new CycleAvoidingMappingContext());

Where objWithCycles is the object of class SomeObjWithCycles which you want to map.

HINT (added 2022-05-27)

Stating the obvious: This solves recursion problems occurring while converting objects using mapstruct. If you have recursion problems using other technologies, e.g. RestEasy, you have to refer to their manuals for solving problems specific to these technologies.

michaeak
  • 1,548
  • 1
  • 12
  • 23
  • 1
    great answer ,office site example link :https://github.com/mapstruct/mapstruct-examples/tree/master/mapstruct-mapping-with-cycles – user1553728 Oct 10 '20 at 06:37
  • how does the generator of mapstruct know when to call `getMappedInstance` and `storeMappedInstance` if both are annotated with the same annotation? – lrxw Oct 16 '20 at 09:02
  • The methods are called with different argument Annotations: `@TargetType` and `@MappingTarget`, both calls are made before the actual mapping is executed. – michaeak Oct 16 '20 at 11:15
  • 1
    These methods don't work if you work with an object builder for your Entity and DTO. I have reported this bug. https://github.com/mapstruct/mapstruct-examples/issues/119 – Flavio Oliva Jul 15 '21 at 12:26
  • 1
    The answer is not clear. How I have to use it?... Assume I have service where I need mapper. I have to call mapper.toDto(entity, context.?(?, ?)) – andrew17 Sep 14 '21 at 12:51
  • 2
    Added usage at the end – michaeak Sep 21 '21 at 11:53
  • I used the approach of context, it solved the recursion/cycle at database layer but the recursion/cycle occcurs at json level because dto A calls dto B thats calls dto A and so on. – cviniciusm May 25 '22 at 18:44
  • it solves the issue at data access level but not at json level. – cviniciusm May 26 '22 at 20:28
  • 1
    @cviniciusm The mappers don’t care whether it is data access or JSON, it is not bound to technology or usage. If you have a problem with json this most likely doesn’t have to do with mapstruct. I don’t know how your problem looks like, but if you are trying to have data access and Json solved with a single data model you should have one dedicated model for data access and one for json – michaeak May 27 '22 at 05:26
  • 1
    You're right, thanks. I think I found out the solution for the json issue: [Infinite Recursion with Jackson JSON and Hibernate JPA issue](https://stackoverflow.com/questions/3325387/infinite-recursion-with-jackson-json-and-hibernate-jpa-issue) – cviniciusm Jul 20 '22 at 18:36
8

Actually this approach with a CycleAvoidingMappingContext didn't work for me using MapStruct version 1.3.1. As I couldn't find much working examples I dediced to post my solution here for others to find.

In case of bi-directional relationships such mappings can trigger a StackOverflowError due to circular references.

Example: classes Recipe, Book and Ingredient which are related bidirectionally 1-to-many and many-to-many.

  • A recipe has lots of ingredients, but is mentioned in only 1 book.
  • One Book has lots of recipes in it.
  • An ingredient is only used in 1 recipe (assuming that an Ingredient also has properties which fixes its amount, unit of measure and so on so that it is indeed specific to one recipe only).
    public class Recipe {
        Long id;
        // ... Other recipe properties go here
        Book book;
        Set<Ingredient> ingredients;
    }
    
    public class Book {
        Long id;
        // ... Other book properties go here
        Set<Recipe> recipes;
    }
    
    public class Ingredient {
        Long id;
        // ... Other ingredient properties go here
        Recipe recipe;
    }

I am assuming you would also have DTO classes with identical properties but ofcourse referring to their corresponding DTO classes.

These would be the default Mapper setups (without depending on Spring in this case) for mapping from your entity classes to your DTO classes:

// MapStruct can handle primitive and standard classes like String and Integer just fine, but if you are using custom complex objects it needs some instructions on how it should map these
    @Mapper(uses = {BookMapper.class, IngredientMapper.class})
    public interface RecipeMapper {
        RecipeMapper INSTANCE = Mappers.getMapper( RecipeMapper.class );

        RecipeDTO toDTO(Recipe recipe);

        Recipe toEntity(RecipeDTO recipeDTO);
    }

    @Mapper(uses = {RecipeMapper.class, IngredientMapper.class})
    public interface BookMapper {
        BookMapper INSTANCE = Mappers.getMapper( BookMapper.class );

        BookDTO toDTO(Book book);

        Book toEntity(BookDTO book);
    }

    @Mapper(uses = {RecipeMapper.class, BookMapper.class})
    public interface IngredientMapper {
        IngredientMapper INSTANCE = Mappers.getMapper( IngredientMapper.class );

        IngredientDTO toDTO(Ingredient ingredient);

        Ingredient toEntity(IngredientDTO ingredientDTO);
    }

If you would stop there and try to map the classes this way, you will get hit by the StackOverflowError due to circular references you have now defined (Recipe contains ingredients which has a property recipe which has ingredients...). Such default Mapper setups can only be used if there is no bidirectional relationship which would trigger the inverse mapping as well.

You could write this down like A -> B -> A -> B -> A ... Concerning the object mapping, my experience has shown that you should be able to map this out like: A -> B -> A (excluding the relations this time to break the cycle) for both Entity to DTO and DTO to Entity mappings. This enables you to:

  • Drill down to associated objects in the frontend: ex. display a list of ingredients for a recipe
  • Persist the inverse relation when saving an object: ex. If you would only map A -> B. The IngredientDTOs inside RecipeDTO would not have a recipe property and when saving an ingredient you would need to pass in the recipe id as a parameter and jump through some hoops to associate the ingredient entity object with a recipe entity object before saving the ingredient entity to the database.

Defining the mappings like A -> B -> A (excluding the relations this time to break the cycle) will boil down to defining separate mappings for when you want to exclude the related complex objects from the mapping at the point where you want to break the cycle.

@IterableMapping(qualifiedByName = "<MAPPING_NAME>") is used to map a collection of complex objects, which refers to a mapping for a single complex object.

@Mapping(target = "PropertyName", qualifiedByName = "<MAPPING_NAME>") can be used to point to an alternative mapping which excludes the inverse relationships when mapping a collection of complex objects (when you want to break the cycle)

@Mapping(target = "[.]", ignore = true) can be used to indicate that a property of an object should not be mapped at all. So this can be used to completely leave out a (collection of) complex object(s) entirely or to ignore properties inside of a single (not a collection) related complex objects directly in case they are not needed.

If you don't use the qualifiedByName attribute and the matching @Named() annotations, your mappings will not compile with an error about Ambiguous mappings if you have multiple methods with the same return type and input parameter types in the Mapper interface.

It may be a good practice use method names matching the @Named annotation value in case you use named mappings.

So, we will note down the wanted behavior first and then code it:

1. When mapping a Recipe, we will need to map the book property in such a way that its inverse relation to recipes is mapped without the book property the second time
    Recipe A -> Book X  -> Recipe A (without book property value as this would close the cycle)
        -> Recipe B (without book property value, as same mapping is used for all these recipes unfortunately as we don't know up front which one will cause the cyclic reference)...
            -> Ingredients I (without recipe property value as they would all point back to A)
                             
2. When mapping a Book, we will need to map the recipes property in such a way that its inverse relation to book isn't mapped as it will point back to the same book.
        Book X -> Recipe A (without book property as this would close the cycle)
                    -> Ingredients (without recipe property as all these will point back to Recipe A)
                        -> Recipe B (without book property, as same mapping is used for all these and all could potentially close the cycle)
                        -> Recipe C
                
3. When mapping an Ingredient, we will need to map the recipe property in such a way that its inverse relation to ingredient isn't mapped as one of those ingredients will point back to the same ingredient

The book property inside the recipe will need to be mapped without the recipes property as one of those will also loop back to the recipe.

    @Mapper(uses = {BookMapper.class, IngredientMapper.class})
    public interface RecipeMapper {
        RecipeMapper INSTANCE = Mappers.getMapper( RecipeMapper.class );

        @Named("RecipeSetIgnoreBookAndIngredientChildRecipes")
        @IterableMapping(qualifiedByName = "RecipeIgnoreBookAndIngredientChildRecipes")
        Set<RecipeDTO> toDTOSetIgnoreBookAndIngredientChildRecipes(Set<Recipe> recipes);

        @Named("RecipeSetIgnoreIngredientsAndBookChildRecipe")
        @IterableMapping(qualifiedByName = "RecipeIgnoreIngredientsAndBookChildRecipe")
        Set<RecipeDTO> toDTOSetIgnoreIngredientsAndBookChildRecipe(Set<Recipe> recipes);
                                
        // In this mapping we will ignore the book property and the recipe property of the Ingredients to break the mapping cyclic references when we are mapping a book object
        // Don't forget to add the matching inverse mapping from DTO to Entity, this is basically just a copy with switch input parameter and return types
        @Named("RecipeIgnoreBookAndIngredientChildRecipes")
        @Mappings({
            @Mapping(target = "book", ignore = true),                                               // book is a single custom complex object (not a collection), so we can directly ignore its child properties from there
            @Mapping(target = "ingredients", qualifiedByName = "IngredientSetIgnoreRecipes"),       // ingredients is a collection of complex objects, so we can't directly ignore its child properties as in the end, a Mapper needs to be defined to Map a single POJO into another
        })
        RecipeDTO toDTOIgnoreBookAndIngredientChildRecipes(Recipe recipe);

        @Named("RecipeIgnoreIngredientsAndBookChildRecipe")
        @Mappings({
            @Mapping(target = "book.recipes", ignore = true),
            @Mapping(target = "ingredients", ignore = true),
        })
        RecipeDTO toDTOIgnoreIngredientsAndBookChildRecipe(Recipe recipe);

        // Don't forget to add the matching inverse mapping from DTO to Entity, this is basically just a copy with switch input parameter and return types
        @Mappings({
            @Mapping(target = "book.recipes", ignore = true),                                       // book is a single custom complex object (not a collection), so we can directly ignore its child properties from there
            @Mapping(target = "ingredients", qualifiedByName = "IngredientSetIgnoreRecipes"),       // ingredients is a collection of complex objects, so we can't directly ignore its child properties as in the end, a Mapper needs to be defined to Map a single POJO into another
        })
        RecipeDTO toDTO(Recipe recipe);
        
        @Named("RecipeSetIgnoreBookAndIngredientChildRecipes")
        @IterableMapping(qualifiedByName = "RecipeIgnoreBookAndIngredientChildRecipes")
        Set<Recipe> toEntitySetIgnoreBookAndIngredientChildRecipes(Set<RecipeDTO> recipeDTOs);
        
        @Named("RecipeSetIgnoreIngredientsAndBookChildRecipe")
        @IterableMapping(qualifiedByName = "RecipeIgnoreIngredientsAndBookChildRecipe")
        Set<Recipe> toEntitySetIgnoreIngredientsAndBookChildRecipe(Set<RecipeDTO> recipeDTOs);
        
        @Mappings({
            @Mapping(target = "book.recipes", ignore = true),                                       // book is a single custom complex object (not a collection), so we can directly ignore its child properties from there
            @Mapping(target = "ingredients", qualifiedByName = "IngredientSetIgnoreRecipes"),       // ingredients is a collection of complex objects, so we can't directly ignore its child properties as in the end, a Mapper needs to be defined to Map a single POJO into another
        })
        Recipe toEntity(RecipeDTO recipeDTO);
        
        @Named("RecipeIgnoreBookAndIngredientChildRecipes")
        @Mappings({
            @Mapping(target = "book", ignore = true),                                               // book is a single custom complex object (not a collection), so we can directly ignore its child properties from there
            @Mapping(target = "ingredients", qualifiedByName = "IngredientSetIgnoreRecipes"),       // ingredients is a collection of complex objects, so we can't directly ignore its child properties as in the end, a Mapper needs to be defined to Map a single POJO into another
        })
        Recipe toEntityIgnoreBookAndIngredientChildRecipes(RecipeDTO recipeDTO);
        
                                @Named("RecipeIgnoreIngredientsAndBookChildRecipe")
        @Mappings({
            @Mapping(target = "book.recipes", ignore = true),
            @Mapping(target = "ingredients", ignore = true),
        })
        Recipe toEntityIgnoreIngredientsAndBookChildRecipe(RecipeDTO recipeDTO);
        
    }



    @Mapper(uses = {RecipeMapper.class, IngredientMapper.class})
    public interface BookMapper {
        BookMapper INSTANCE = Mappers.getMapper( BookMapper.class );
        
        @Mappings({
            @Mapping(target = "recipes", qualifiedByName = "RecipeSetIgnoreBookAndIngredientChildRecipes"),
        })
        BookDTO toDTO(Book book);

        @Mappings({
            @Mapping(target = "recipes", qualifiedByName = "RecipeSetIgnoreBookAndIngredientChildRecipes"),
        })
        Book toEntity(BookDTO book);
    }



    @Mapper(uses = {RecipeMapper.class, BookMapper.class})
    public interface IngredientMapper {
        IngredientMapper INSTANCE = Mappers.getMapper( IngredientMapper.class );

        // Don't forget to add the matching inverse mapping from DTO to Entity, this is basically just a copy with switch input parameter and return types
        @Named("IngredientSetIgnoreRecipes")
        IterableMapping(qualifiedByName = "IngredientIgnoreRecipes")                                // Refer to the mapping for a single object in the collection
        Set<IngredientDTO> toDTOSetIgnoreRecipes(Set<Ingredient> ingredients);

        // Don't forget to add the matching inverse mapping from DTO to Entity, this is basically just a copy with switch input parameter and return types
        @Named("IngredientIgnoreRecipes")
        @Mappings({
            @Mapping(target = "recipes", ignore = true),                                            // ignore the recipes property entirely
        })
        IngredientDTO toDTOIgnoreRecipes(Ingredient ingredient);

        @Mappings({
            @Mapping(target = "recipes", qualifiedByName = "RecipeSetIgnoreIngredientsAndBookChildRecipe")
        })
        IngredientDTO toDTO(Ingredient ingredient);

        @Named("IngredientSetIgnoreRecipes")
        IterableMapping(qualifiedByName = "IngredientIgnoreRecipes")                                // Refer to the mapping for a single object in the collection
        Set<Ingredient> toEntitySetIgnoreRecipes(Set<IngredientDTO> ingredientDTOs);

        @Named("IngredientIgnoreRecipes")
        @Mappings({
            @Mapping(target = "recipes", ignore = true),
        })
        Ingredient toEntityIgnoreRecipes(IngredientDTO ingredientDTO);

        @Mappings({
            @Mapping(target = "recipes", qualifiedByName = "RecipeSetIgnoreIngredientsAndBookChildRecipe")
        })
        Ingredient toEntityIgnoreRecipes(IngredientDTO ingredientDTO);
    }

Usage

<ENTITY_NAME>DTO <eNTITY_NAME>DTO = <ENTITY_NAME>Mapper.INSTANCE.toDTO( <eNTITY_NAME> );`
Ivo Eersels
  • 101
  • 1
  • 2
  • Ignoring something for the mapping is not the same… The approach using the context worked for others. what went wrong for you? – michaeak Sep 21 '21 at 12:04
  • I used the approach of context, it solved the recursion/cycle at database layer but the recursion/cycle occcurs at json level because dto A calls dto B thats calls dto A and so on. – cviniciusm May 25 '22 at 18:44
4

There is no detection or special handling of cases like this in MapStruct yet, but there is a feature request for it: #469. If you got any ideas how to deal with cycles, please drop a comment at that issue.

Gunnar
  • 18,095
  • 1
  • 53
  • 73
  • I wish i did! One could either do a-la-Hibernate: use a wrapper class and when the getter is called convert it on the spot. Or, as stated by the creator of the issue, use a map to hold already converted items, and when you find an object already converted earlier during the conversion process use that one in the setter rather than convert a new one. But i'm really not expert enough to make such suggestions with ease – valepu Mar 31 '16 at 08:58
  • I think this answer is out-dated – inrandomwetrust May 22 '23 at 18:38
2

The answer was not quite easy to find on this page, so I will just post what is working in my case to prevent cyclic references.

Answer from George Siggouroglou was working fine using following :

@Mapping(target = "primaryObject.secondaries", expression = "java(null)"),
SecondaryObjectDto toSecondaryObjectDto(SecondaryObject source);

Answer from Ivo Eersel was very complete, but I still managed to miss the solution on the first read.

So here is what I finally used :

@Mapping(target = "primaryObject.secondaries", ignore = true)
SecondaryObjectDto toSecondaryObjectDto(SecondaryObject source);
DependencyHell
  • 1,027
  • 15
  • 22