1. Overview

When we work with generics, Java enforces type safety only during the compilation time. At runtime, Java erases type information and we lose the details of the type parameters. Because of this, we need special support when mapping generic types. In this tutorial, we're going to look at how we can map generic classes and collections using Orika.

2. Sample Application

Let's first see our sample classes.

We have the Person and PersonDto classes:

public class Person {

    private String name;

    // Getters and setters...
}

public class PersonDto {

    private String name;

    // Getters and setters...
}

They both have a single field: name.

3. Map Generic Classes

We'll now examine how to map generic classes.

We'll introduce the Holder and AnotherHolder classes:

public class Holder<T> {

    private T value;

    // Getters and setters...
}

public class AnotherHolder<T> {

    private T value;

    // Getters and setters...
}

The Holder class has a type parameter T and declares the value field. The same goes for AnotherHolder.

To define a mapper for Holder and AnotherHolder, we'll use the ClassMapBuilder API and pass the raw types. Remember that Holder is the raw type, whereas Holder<T> is the generic type:

MapperFactory factory = new DefaultMapperFactory.Builder().build();
factory.classMap(Holder.class, AnotherHolder.class).byDefault().register();

We'll next test our mapper:

@Test
public void testParameterizedClass_UsingRawTypes() {
    MapperFactory factory = new DefaultMapperFactory.Builder().build();
    factory.classMap(Holder.class, AnotherHolder.class).byDefault().register();

    Person person = new Person();
    person.setName("Name");
    final Holder<Person> holder = new Holder<>();
    holder.setValue(person);

    final AnotherHolder<?> anotherHolder = factory.getMapperFacade().map(holder, AnotherHolder.class);

    assertThat(anotherHolder.getValue()).isNotInstanceOfAny(Person.class, PersonDto.class);
}

In this test, we have a Holder instance containing a Person. When we perform the mapping, we get an AnotherHolder instance. But it doesn't have a Person instance as its value. Instead, it contains an Object instance losing the initial Person data.

To support the mapping of generic types, Orika provides the TypeBuilder API. The Type and TypeBuilder classes allow us to notify Orika about the generic type parameters:

@Test
public void testParameterizedCollection_UsingGenericTypes() {
    final MapperFactory factory = new DefaultMapperFactory.Builder().build();
    final Type<Holder<Person>> sourceType = new TypeBuilder<Holder<Person>>() {}.build();
    final Type<AnotherHolder<PersonDto>> targetType = new TypeBuilder<AnotherHolder<PersonDto>>() {}.build();
    factory.classMap(sourceType, targetType)
      .byDefault()
      .register();

    Person person = new Person();
    person.setName("Name");
    final Holder<Person> holder = new Holder<>();
    holder.setValue(person);

    final AnotherHolder<PersonDto> anotherHolder = factory.getMapperFacade().map(holder, sourceType, targetType);

    assertThat(anotherHolder.getValue().getName()).isEqualTo(holder.getValue().getName());
}

In this modified version, we're specifying the generic types - Holder<Person> and AnotherHolder<PersonDto> - instead of raw types. Note the usage of TypeBuilder - new TypeBuilder<Holder<Person>>() {}. In essence, we're creating an anonymous class and fixing the type parameter so that Orika can detect the actual type parameter. Another difference is that we're using the map method that accepts the source and target types - sourceType and targetType.

4. Map Parameterized Collections

Next, we'll see how to map parameterized collections using Orika. Similar to the previous example, if the type parameter isn't actualized - or fixed - Orika can't map the Collection-typed fields correctly.

Let's see an example:

public class Group<P> {

    private List<P> members = new ArrayList<>();

    // Getters and setters...
}

public class GroupDto<P> {

    private List<P> members = new ArrayList<>();

    // Getters and setters...
}

Here, we have two generic classes, Group and GroupDto, with a type parameter P. Additionally, both classes declare a list field - List<P> members.

We'll define a mapper next:

@Test
public void testParameterizedCollection_UsingRawTypes() {
    MapperFactory factory = new DefaultMapperFactory.Builder().build();
    factory.classMap(Group.class, GroupDto.class).byDefault().register();

    Person person = new Person();
    person.setName("Name");
    Group<Person> group = new Group<>();
    group.setMembers(Arrays.asList(person));

    final GroupDto<PersonDto> groupDto = factory.getMapperFacade().map(group, GroupDto.class);

    assertThat(groupDto.getMembers().get(0)).isNotInstanceOfAny(Person.class, PersonDto.class);
}

In the mapper configuration, we're creating a ClassMap using the raw types - classMap(Group.class, GroupDto.class). Then we're initializing a Group instance holding a Person list. After the mapping, the resulting GroupDto object contains a List but the Person data is lost. Furthermore, we don't normally notice this until we access the data at runtime:

final PersonDto personDto = groupDto.getMembers().get(0); // Throws ClassCastException

Since the compiler expects the list element to be of type PersonDto, it puts an implicit cast. But in fact, the element is an Object instance. Thus the code throws a ClassCastException at runtime:

java.lang.ClassCastException: java.lang.Object cannot be cast to 
com.javabyexamples.java.mapper.orika.generics.collections.PersonDto

To solve this problem, we must provide the necessary type information to Orika using the TypeBuilder class:

@Test
public void testParameterizedCollection_UsingGenericTypes() {
    MapperFactory factory = new DefaultMapperFactory.Builder().build();
    Type<Group<Person>> sourceType = new TypeBuilder<Group<Person>>() {
    }.build();
    Type<GroupDto<PersonDto>> targetType = new TypeBuilder<GroupDto<PersonDto>>() {
    }.build();
    factory.classMap(sourceType, targetType).byDefault().register();

    Group<Person> group = // Get group.

    GroupDto<PersonDto> groupDto = factory.getMapperFacade().map(group, sourceType, targetType);

    assertThat(groupDto.getMembers().get(0)).isInstanceOf(PersonDto.class);
    assertThat(groupDto.getMembers().get(0).getName()).isEqualTo(group.getMembers().get(0).getName());
}

Here, by specifying the generic types, we're declaring that we'll map Group<Person> to GroupDto<PersonDto>. As a result, Orika maps the Group and Person instances to the correct target types.

In the Group class, the collection's actual type parameter changes whenever a Group with a new P is created. However, if we establish the actual type argument in the class definition, the type details become available at runtime for the enclosing class - PersonGroup and PersonGroupDto. So Orika can map the collection correctly:

public class PersonGroup {

    private List<Person> members = new ArrayList<>();

    // Getters and setters...
}

public class PersonGroupDto {

    private List<PersonDto> members = new ArrayList<>();

    // Getters and setters...
}

@Test
public void testCollection_UsingRawTypes() {
    MapperFactory factory = new DefaultMapperFactory.Builder().build();
    factory.classMap(PersonGroup.class, PersonGroupDto.class).byDefault().register();

    // Other code...
}

Here, unlike GroupPersonGroup isn't generic and always holds a list of Person instances. So to map PersonGroup and PersonGroupDto instances, we can just supply the raw types when invoking classMap.

5. Map Heterogeneous Collections

In the previous examples, we created collections that store the same type of elements. Conversely, if we create a heterogeneous collection, we give up on the type details:

public class Inventory {

    private Map<String, ?> data = new HashMap<>();

    // Getters and setters...
}

public class InventoryDto {

    private Map<String, ?> data = new HashMap<>();

    // Getters and setters...
}

Here, we're defining the Inventory and InventoryDto classes. Note that the data map uses a wildcard to represent its value type. In essence, the question mark tells that data can store any type of value.

In cases where type information can't be retrieved correctly, Orika can't perform a correct mapping:

@Test
public void testParameterizedCollection() {
    MapperFactory factory = new DefaultMapperFactory.Builder().build();
    factory.classMap(Inventory.class, InventoryDto.class).byDefault().register();

    final HashMap<String, Object> map = new HashMap<>();
    map.put("numbers", Arrays.asList("1", "2", "3"));
    final Inventory inventory = new Inventory();
    inventory.setData(map);

    final InventoryDto inventoryDto = factory.getMapperFacade().map(inventory, InventoryDto.class);

    assertThat(inventoryDto.getData().get("numbers")).isNotInstanceOf(List.class);
}

In this test, we're putting a list of Strings into the data map under the key, numbers. Then we're mapping the Inventory instance to InventoryDto. Normally we expect to see the same list in the InventoryDto's data field. But instead, we get a bare Object instance without any value.

The solution to this problem can change according to the application. If we know the structure of the collection - perhaps via some convention -  we can create a CustomMapper to copy the contents manually.

6. Summary

In this tutorial, we looked at Orika's generics support. Firstly, we defined new generic types and detailed the usage of the TypeBuilder API. Then we examined how Orika maps the parameterized collections.

As always, the source code for all examples in this tutorial is available on Github.