MapStruct Library – Automatically Generate Java Mapping Code

Java class mapping

When developing APIs, we had to write a lot of mapping code for converting response classes into different class formats or converting entity and DTO classes into plain Java classes.

Below is an example of a normal mapper where we are mapping Employee DTO into Employee class.


public class Mapper {

	public Employee toEmployee(EmployeeDTO dto) {

		Employee employee = new Employee();

		employee.setName(dto.getName());
		employee.setJobTitle(dto.getJobTitle());
		employee.setJoinedDate(dto.getJoinedDate());
		employee.setMobileNo(dto.getMobileNo());
		employee.setDepartment(dto.getDepartment());
		employee.setSalary(dto.getSalary());

		return employee;
	}
}

If source and target classes have the same property name and structure, we can automate the mapper code using the MapStruck libray in Java.

What is MapStruck?

MapStruct is a code generator that greatly simplifies the implementation of mappings between Java models and DTO. Using Mapstruck we don’t have to write a lot of mapper code. Instead, we simply need to say source and destination class.

MapStruck automatically generates mapper code which maps fields based on name from source to destination class, and we can pass custom mapping for fields.

Mapstruck maven dependency

Add the below dependency in pom.xml.


<org.mapstruct.version>1.5.2.Final</org.mapstruct.version>

<dependency>
	<groupId>org.mapstruct</groupId>
	<artifactId>mapstruct</artifactId>
	<version>${org.mapstruct.version}</version>
</dependency>

If you are using Lombok to reduce boilerplate code in an entity or DTO class then add below <annotationProcessorPaths> in pom.xml. 


 	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
					<annotationProcessorPaths>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
							<version>${org.projectlombok.version}</version>
						</path>
						<path>
							<groupId>org.mapstruct</groupId>
							<artifactId>mapstruct-processor</artifactId>
							<version>${org.mapstruct.version}</version>
						</path>
					</annotationProcessorPaths>
				</configuration>
			</plugin>
		</plugins>
	</build>
  

How to create a mapper in Mapstruck?1

Create an interface and annotate with @Mapper. Now let's define a method. The method parameter acts as source and the method return type acts as Target. MapStruck will map source fields to target fields if they have the same field name.

Mapping fields with the same name

By default, MapStruck maps fields by name. If source and target have fields with the same name, the mapper maps the field. The EmployeeDTO and Employee classes both have the same property names.


@Mapper(componentModel = "spring")
public interface EmployeeMapper {

	Employee toEmployee(EmployeeDTO employeeDTO);

}

The componentModel="spring" defines this mapper as a spring bean which we can autowire into our application.

The generated class code will look like below, with the source and target classes both having the same property names.


@Component
public class EmployeeMapperImpl implements EmployeeMapper {

	@Override
	public Employee toEmployee(EmployeeDTO employeeDTO) {
		if (employeeDTO == null) {
			return null;
		}

		Employee employee = new Employee();

		employee.setDepartment(employeeDTO.getDepartment());
		employee.setGender(employeeDTO.getGender());
		employee.setId(employeeDTO.getId());
		employee.setJobTitle(employeeDTO.getJobTitle());
		employee.setJoinedDate(employeeDTO.getJoinedDate());
		employee.setMobileNo(employeeDTO.getMobileNo());
		employee.setName(employeeDTO.getName());
		employee.setSalary(employeeDTO.getSalary());

		return employee;
	}
}

Mapping fields with different names

In most cases, the target and source classes will not have the same field names. Therefore, how can we map these fields? This can be achieved by using a @Mapping annotation, which takes the target field name and the source field name and maps them. We can provide custom mapping logic for fields in the @Mapping annotation.

@Mappings takes a group of @Mapping annotations. If there is more mapping logic, it would be better to put it inside @Mappings.


public interface EmployeeMapper {

	@Mappings({
		@Mapping(target = "jobTitle", source = "title"),
		@Mapping(target = "joinedDate", source = "startingdate")
	})
	Employee toEmployee(EmployeeDTO employeeDTO);

}

Mapping nested beans fields

Here we will see how we can map nested object fields. For example, the Employee class has a Personal object which has name, gender, age and Address object.

We have to give full path to the field example personal.address.streetaddress in target, so mapper will search for streetaddress inside address in personal object.


@Mappings({ 
	@Mapping(target = "personal.name", source = "firstName"),
	@Mapping(target = "personal.gender", source = "gender"), 
	@Mapping(target = "personal.age", source = "age"),
	@Mapping(target = "personal.address.streetaddress", source = "dto.primaryAddr.line1"),
	@Mapping(target = "personal.address.city", source = "dto.primaryAddr.city"),
	@Mapping(target = "personal.address.postalcode", source = "dto.primaryAddr.postalCode") 
})
Employee toEmployee(EmployeeDTO dto);
    

Mapping custom method

In some cases, there can be a need to manually implement a mapping from one type to another that cannot be generated by MapStruct. You can implement the custom method on another class and then use it in mappers generated by MapStruct

Define the Java 8 default method in the interface and provide the implementation. We use @Named to name the method.

In the @Mapping define the source and target, give the @Named method name in the qualifiedByName attributes so the mapstruck will generate the code and call the default method


import java.util.ArrayList;
import java.util.List;

@Mapper(componentModel = "spring")
public interface EmployeeMapper {

	@Mapping(target = "primaryAddr", source = "personal.address", qualifiedByName = "addressdto")
	EmployeeDTO toEmployeeDto(Employee employee);

	@Named("addressdto")
	default PrimaryAddr composeAddress(Address address) {

		PrimaryAddr primaryAddr = new PrimaryAddr();

		primaryAddr.setLine1(address.getStreetaddress());
		primaryAddr.setCity(address.getCity());
		primaryAddr.setPostalCode(address.getPostalcode());

		return primaryAddr;
	}
}

Let's see how mapstruck generated the mapper code for the above custom method. Internally, the generated mapper code calls our custom method for mapping addresses.


public class EmployeeMapperImpl implements EmployeeMapper {

	@Override
	public EmployeeDTO toEmployeeDto(Employee employee) {
		if (employee == null) {
			return null;
		}

		EmployeeDTO employeeDTO = new EmployeeDTO();

		employeeDTO.setPrimaryAddr(composeAddress(employeePersonalAddress(employee)));

		return employeeDTO;
	}

	private Address employeePersonalAddress(Employee employee) {
		if (employee == null) {
			return null;
		}
		Personal personal = employee.getPersonal();
		if (personal == null) {
			return null;
		}
		Address address = personal.getAddress();
		if (address == null) {
			return null;
		}
		return address;
	}
}

Post a Comment

Previous Post Next Post

Recent Posts

Facebook