JVM Advent

The JVM Programming Advent Calendar

Making data transformation easy with MapStruct

Motivation

Most applications in the world follow a simple format. They ingest some data, do some transformation on it, and then expose this transformed data to their consumers. If we look at a high enough level, we will see this pattern in all applications. The transformation part can be very complex and follow a lot of business logic, or it could be very simple and consist of just renaming some fields, removing some fields or adding some fields. As we all can guess, writing code for simple transformation can feel very repetitive, boring and thus error-prone. So, it comes as no surprise that there are libraries out there, built to tackle this in an easier and simpler way. MapStruct is just one library like that one.
My team used it a long time ago, on a project where our application needed to read data from DB and expose it to the world over REST API. The problem was that we were not the owners of the DB and we were not able to change DB schema definitions and make data look in DB how it looked to end consumers of our API. So, we were faced with two possible solutions to our problem. Write a lot of boilerplate code to do transformations, and invoke a lot of maintenance costs on ourselves, or use a library that would do most of the heavy lifting for us. Our choice landed on MapStruct, and we were saved on multiple occasions by it, especially as the requirements of our consumers underwent massive changes. Changing transformations was easy because we only needed to change some settings and MapStruct did all the work for us automatically.
Let us take a look at MapStruct.

Context

In our example, we shall assume that we need to build a REST API that exposes some data that don’t belong to us. We are either receiving it via some 3rd party API or reading from some other team DB.
To make it self-contained, In our use case, we will assume that data is coming from DB. For simplicity reasons in this example, we will use H2, in the memory database to simulate this use case. Let us assume that we are interested in Customer data, which is stored in table Customers.
The first thing that we need to do, is to create a simple Java class Customer,  mark it as Entity and map all columns from the table into this class. Once we are done, we will get some code like this.
@Entity
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @NotNull
    private String firstName;
    @NotNull
    private String lastName;
    @NotNull
    private Integer dayOfBirth;
    @NotNull
    private Integer monthOfBirth;
    @NotNull
    private Integer yearOfBirth;
    private String address;
    private Integer houseNumber;
    private String houseNumberAddition;
    private String city;
    private String country;
    // getters and setters
    .....
}
Unfortunately, our consumers are expecting data in different format. So, let us create class Customer2DTO which will be in the format that our consumers expect to receive the data.
We would end up with code like this.
public class Customer2DTO {
    private Long id;

    private String name;
    private String familyName;
    private String fullName;
    private LocalDate birthDay;

    private String address;
    private Integer houseNumber;
    private String houseNumberAddition;
    private String city;
    private String country;
    // getters and setters
    ....
}
As we can see, some fields are the same in both Customer and Customer2DTO. However, some fields have different names in Customer2DTO like name and familyName. Also, there are fields in Customer2DTO that don’t exist in Customer, fullName and birthDay. Of course, we can write code that would copy all the same fields, and adjust for name changes, this would mean that we would increase the size of our code, and the amount of code that we now need to maintain and look after. A more elegant solution would be to use MapStruct, so let us see how we can do exactly that in this use case.

Adding MapStruct to our project

The first thing that we need to do, is to add MapStruct to our project as a dependency.
Let us add this piece of code in the dependencies block of our pom.xml file
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>1.5.5.Final</version>
        </dependency>
Since MapStruct will generate code for us, we need to tell Maven to invoke MapStruct during compile time for the generation of code to happen. We can achieve this by extending the build plugin phase, by adding this piece of code in pom.xml
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
Now that MapStruct has been added to our project, and the build phase adjusted, we can start leveraging the power of MapStruct.

First usage of MapStruct

We need to create one interface, let us call it Customer2Mapper, and let us annotate it with @Mapper.
In this way, we are telling MapStruct to create an implemenation of this interface.
import org.mapstruct.Mapper;

@Mapper
public interface Customer2Mapper {
}
The next thing is to get INSTANCE of this interface that we can use in our code. For that, we need to add one line to our code.
    Customer2Mapper INSTANCE = Mappers.getMapper(Customer2Mapper.class);
The final thing that we need to do is to create a method signature that will do the transformation from Customer to Customer2DTO.
    Customer2DTO customerToCustomerDTO(Customer customer);
full code will look something like this
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.dto.Customer2DTO;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.pojo.Customer;

@Mapper
public interface Customer2Mapper {

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

    Customer2DTO customerToCustomerDTO(Customer customer);
}
To utilise MapStruct transformation, we just need to call this code on the instance of a Customer.
Customer2DTO c2dto = Customer2Mapper.INSTANCE.customerToCustomerDTO(customer);
In case we run this code and check what the result is, we will see that MapStruct did its best to map all the fields from Customer to Customer2DTO which have the same name and compatible types. In our case, all fields should be mapped, except name, familyName, fullName and birthDay.

Renaming fields

Let us see how we can use MapStruct to rename the fields.
All that we need to do, to tell MapStruct to map field firstName from Customer to field name in Customer2DTO, is to add this line before the method signature of customerToCustomerDTO
    @Mapping(source = "firstName", target = "name")
here we are telling MapStruct to use field firstName in Customer as source and field name in Customer2DTO as target. MapStruct will generate code that will do exactly that. We can repeat this for as many fields as we want, we just need to match the correct source and target fields. So for mapping lastName to familyName we need to add this
    @Mapping(source = "lastName", target = "familyName")

Combining multiple fields

In the case of the fullName, we need to combine two fields from Customer into one field in Customer2DTO. Again we will use annotation @Mapping. Parameter target will be fullName. However, we will not use source. Instead, we will use expresion. The code will look like this
    @Mapping(target="fullName", expression = "java(customer.getFirstName() +\" \"+ customer.getLastName())")
We can put almost any Java code in expresion. However, it would make most sense to keep it simple and don’t go nuts. Think about the future you who will maintain this code :-).
To map the field birthDay in Customer2DTO, we will use a similar approach as for fullName. We will add this code
    @Mapping(target="birthDay", expression = "java(java.time.LocalDate.of(customer.getYearOfBirth(), customer.getMonthOfBirth(), customer.getDayOfBirth()))")
so full code will look something like this
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.dto.Customer2DTO;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.pojo.Customer;

@Mapper
public interface Customer2Mapper {

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

    @Mapping(source = "firstName", target = "name")
    @Mapping(source = "lastName", target = "familyName")
    @Mapping(target="fullName", expression = "java(customer.getFirstName() +\" \"+ customer.getLastName())")
    @Mapping(target="birthDay", expression = "java(java.time.LocalDate.of(customer.getYearOfBirth(), customer.getMonthOfBirth(), customer.getDayOfBirth()))")
    Customer2DTO customerToCustomerDTO(Customer customer);
}

Creating “sub-objects” inside Data Transfer objects

Very often once we have done some work, requirements change or get extended. So let us assume that happened to our use case. Instead of getting data in the format of Customer2DTO, all of a sudden, we need to send data in a different format. First, we will create a class in a new format and will call it Customer3DTO. It would look something like this
public class HomeAddressDTO {
    private String street;
    private Integer houseNumber;
    private String addition;
    private String city;
    private String country;
    //getters and setters
    ....
}
public class Customer3DTO {
    private Long id;
    private String name;
    private String familyName;
    private String fullName;
    private LocalDate birthDay;

    private HomeAddressDTO homeAddress;
    // getters and setters
    ....
}
as we can see, info about home address now needs to be an object in itself, instead of individual fields in response of our API.
Let us make a copy of our Mapper interface Customer2Mapper, and let us call it Customer3Mapper. The only thing that we need to do to create an instance of object HomeAddressDTO in Customer3DTO and fill in with appropriate data is to add a few mappings that we used in past with source and target arguments. Only this time values for the target will have the prefix “homeAddress.” . The result should look like this
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.dto.Customer3DTO;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.pojo.Customer;

@Mapper
public interface Customer3Mapper {

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

    @Mapping(source = "firstName", target = "name")
    @Mapping(source = "lastName", target = "familyName")
    @Mapping(target="fullName", expression = "java(customer.getFirstName() +\" \"+ customer.getLastName())")
    @Mapping(target="birthDay", expression = "java(java.time.LocalDate.of(customer.getYearOfBirth(), customer.getMonthOfBirth(), customer.getDayOfBirth()))")

    @Mapping(target="homeAddress.street",source="address")
    @Mapping(target="homeAddress.houseNumber",source="houseNumber")
    @Mapping(target="homeAddress.addition",source="houseNumberAddition")
    @Mapping(target="homeAddress.city",source="city")
    @Mapping(target="homeAddress.country",source = "country")

    Customer3DTO customerToCustomerDTO(Customer customer);
}
If we run this code and check output we should see that all is as expected. We can easily check this by using simple JUnit tests, for example
public class DummyCustomerBuilder {
    public static Customer dummyCustomer() {
        Customer customer = new Customer();
        customer.setId((long)1);
        customer.setFirstName("Sherlock");
        customer.setLastName("Holmes");
        customer.setCity("London");
        customer.setCountry("Great Britan");
        customer.setHouseNumber(221);
        customer.setHouseNumberAddition("B");
        customer.setAddress("Baker Street");
        customer.setDayOfBirth(6);
        customer.setMonthOfBirth(1);
        customer.setYearOfBirth(1854);

        return customer;
    }
}
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.dto.Customer3DTO;
import xyz.itshark.blog.mapstructdemo.mapstructdemo.pojo.Customer;

import java.time.LocalDate;

public class Customer3MapperTest {
 // .........
 
    @Test
    public void testMappingHomeAddress() {
        //given
        Customer customer = DummyCustomerBuilder.dummyCustomer();

        //when
        Customer3DTO cDto = Customer3Mapper.INSTANCE.customerToCustomerDTO(customer);

        //then
        Assertions.assertNotNull(cDto);
        Assertions.assertNotNull(cDto.getHomeAddress());
        Assertions.assertEquals("Baker Street",cDto.getHomeAddress().getStreet());
        Assertions.assertEquals(Integer.valueOf(221), cDto.getHomeAddress().getHouseNumber());
        Assertions.assertEquals("B",cDto.getHomeAddress().getAddition());
        Assertions.assertEquals("London", cDto.getHomeAddress().getCity());
        Assertions.assertEquals("Great Britan",cDto.getHomeAddress().getCountry());
    }
}

Conclusion

As we saw from our simple realistic example, with very little code using MapStruct we can handle a lot of everyday transformation without the need to create a lot of boilerplate code. This means that maintaining and modifying code to meet the demands of tomorrow will be easier, due to the simple fact that there is less of it.
In this blog post, we just scratched the surface of all the things MapStruct can help us with, and I highly recommend checking [official website](https://mapstruct.org/) for more info.
My suggestion in day-to-day usage of MapStruct would be make sure to keep it simple and always think if something is easier and better to achieve using MapStruct or your custom code. The fact that you can do something using one, doesn’t always mean that you should. Maybe there is a simpler and better solution.

## Resources

Author: Vladimir Dejanovic

Founder and leader of AmsterdamJUG.
JavaOne Rock Star, CodeOne Star speaker
Storyteller

Software Architect ,Team Lead and IT Consultant working in industry since 2006 developing high performance software in multiple programming languages and technologies from desktop to mobile and web with high load traffic.

Enjoining developing software mostly in Java and JavaScript, however also wrote fair share of code in Scala, C++, C, PHP, Go, Objective-C, Python, R, Lisp and many others.

Always interested in cool new stuff, Free and Open Source software.

Like giving talks at conferences like JavaOne, Devoxx BE, Devoxx US, Devoxx PL, Devoxx MA, Java Day Istanbul, Java Day Minks, Voxxed Days Bristol, Voxxed Days Bucharest, Voxxed Days Belgrade, Voxxed Days Cluj-Napoca and others

Next Post

Previous Post

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2024 JVM Advent | Powered by steinhauer.software Logosteinhauer.software

Theme by Anders Norén