How to automate java bean to bean mapping in Spring Boot using MapStruct?

Let’s say you are developing an application which fetches data from the database.

Let’s say you are using hibernate to do the object relational data mapping.

So the data you fetch from database are mapped into Java entity objects .

And then you extract data from this entity object into another java bean object (DTO/data transfer object) as required by your client code.

Traditionally we have been doing this mapping manually using ‘modeler’ classes.

You use getters and setters and manually populate your DTOs from your entity objects.

This is time consuming and can be error prone.

Is there a way to automate this?

Yes !

Using the library MapStruct.

You just need to create an interface where you tell MapStruct the mappings between the two java beans (This property in the first java bean should be mapped to that property in the second java bean) .

MapStruct then automatically populates those objects. It even does the type conversion for most cases by itself (Say one of the properties is an integer in the source java bean object but is a string in the destination java bean) .For cases where the type conversion fails you can tell Mapstruct how to convert it.

We will conver the below items in this post:

  1. A basic example of how to automatically map source java bean values to destination java bean .
  2. How to map properties with different names
  3. How to map nested fields from source java bean to a property in destination java bean
  4. How to update an already populated java bean with values from another java bean

Let’s dive into the implementation.

These are the steps:

STEP1: Add MapStruct dependency to your project

STEP2: Create the source and destination java beans

STEP3: Create a mapper interface

STEP4: Run mvn clean install

STEP5: Test

Let’s look at this in detail.

STEP1: Add MapStruct dependency :

Add the below property, dependency and plugin details to your maven pom.xml:

...
<properties>
    <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
</properties>
...
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>
...
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source> <!-- depending on your project -->
                <target>1.8</target> <!-- depending on your project -->
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                    <!-- other annotation processors -->
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

STEP2: Create the source and destination java beans

Let’s assume that you are developing an application which fetches vaccine details from database.

Lets this be the entity object:

package com.example.demo;

public class Vaccine {

	private String name;
	
	private String manufacturer;
	
	private int quantity;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getManufacturer() {
		return manufacturer;
	}

	public void setManufacturer(String manufacturer) {
		this.manufacturer = manufacturer;
	}

	public int getQuantity() {
		return quantity;
	}

	public void setQuantity(int quantity) {
		this.quantity = quantity;
	}
	
	
}

Let’s assume that the above object gets populated automatically by fetching values from the database.

Now you want to map these values to another DTO object which will be passed to your UI code or any client code:

package com.example.demo;

public class VaccineDTO {

	private String name;
	
	private String manufacturer;
	
	private String quantity;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getManufacturer() {
		return manufacturer;
	}

	public void setManufacturer(String manufacturer) {
		this.manufacturer = manufacturer;
	}

	public String getQuantity() {
		return quantity;
	}

	public void setQuantity(String quantity) {
		this.quantity = quantity;
	}
	
	
}

Notice that the quantity type is string in the DTO object whereas it is integer type in the entity object.

Let’s see if MapStruct does the conversion automatically

STEP3: Create a mapper interface

Create a mapper interface annotated with @Mapper from MapStruct :

package com.example.demo;

import org.mapstruct.Mapper;

@Mapper
public interface VaccineMapper {

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

	VaccineDTO fromVaccine(Vaccine vaccine);
}

The line :

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

is used to create an instance for the mapper which can later be used to convert the object.

The below method:

VaccineDTO fromVaccine(Vaccine vaccine);

does the conversion automatically for us! You can give any name for the method .

That’s it!

Now lets see if it works .

STEP4: Run mvn clean install

Now we need to allow MapStruct to generate the ‘modeler’ classes automatically for us.

This is specified in pom.xml using the annotationProcessorPaths mapping.

For this to work you need to run mvn clean install or mvnw clean install in case you are using maven wrapper (Make sure JAVA_HOME is set in your environment variables for this to work)

After running this a class got generated automatically by MapStruct:

Here is the content of the above class:

package com.example.demo;

import javax.annotation.processing.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-05-31T17:26:50+0530",
    comments = "version: 1.4.2.Final, compiler: javac, environment: Java 16 (Oracle Corporation)"
)
public class VaccineMapperImpl implements VaccineMapper {

    @Override
    public VaccineDTO fromVaccine(Vaccine vaccine) {
        if ( vaccine == null ) {
            return null;
        }

        VaccineDTO vaccineDTO = new VaccineDTO();

        vaccineDTO.setName( vaccine.getName() );
        vaccineDTO.setManufacturer( vaccine.getManufacturer() );
        vaccineDTO.setQuantity( String.valueOf( vaccine.getQuantity() ) );

        return vaccineDTO;
    }
}

MapStruct created a modeler class for us!

And look at the line where the quantity is set to the vaccineDTO . It is converted to a string by MapStruct!

Now let’s test this out!

STEP5: Test

I created a test REST service like this:

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestCotroller {

	
	@GetMapping("/test")
	public VaccineDTO test() {
		
		
		//populated here for demo but assume this is fetched from db:
		Vaccine vaccine = new Vaccine();
		vaccine.setName("Covaxin");
		vaccine.setManufacturer("Adar and Co");
		vaccine.setQuantity(100);
		
		VaccineMapper mapper = VaccineMapper.INSTANCE;
		
		
		VaccineDTO dto = mapper.fromVaccine(vaccine);
		
		
		return dto;
		
	}
}

In the above code the Vaccine object is prepopulated (In a real case scenario this would be fetched from db).

And then I created an instance for the mapper and finally used it to map vaccine object values to vaccine dto object values.

Here is the output on calling the above REST service:

It worked!

Now , what if the source and destination property names are different?

Let’s say in the vaccine class the property name of the manufacturer is ‘manufacturer’ but in the vaccine dto class it is ‘company’:

For this to work , add @Mapping annotation like below with the required source and destination names:

package com.example.demo;

public class Vaccine {

	private String name;
	
	private String manufacturer;
	
	private int quantity;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getManufacturer() {
		return manufacturer;
	}

	public void setManufacturer(String manufacturer) {
		this.manufacturer = manufacturer;
	}

	public int getQuantity() {
		return quantity;
	}

	public void setQuantity(int quantity) {
		this.quantity = quantity;
	}
	
	
}

package com.example.demo;

public class VaccineDTO {

	private String name;
	
	private String company;
	
	private String quantity;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	

	public String getCompany() {
		return company;
	}

	public void setCompany(String company) {
		this.company = company;
	}

	public String getQuantity() {
		return quantity;
	}

	public void setQuantity(String quantity) {
		this.quantity = quantity;
	}
	
	
}

package com.example.demo;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

@Mapper
public interface VaccineMapper {
	
	VaccineMapper INSTANCE = Mappers.getMapper(VaccineMapper.class);

	@Mapping(source="manufacturer",target="company")
	VaccineDTO fromVaccine(Vaccine vaccine);
}

Again run mvn clean install and test your changes.

It worked!

What if you have multiple properties which have different names?

Add multiple @Mapping annotations:

package com.example.demo;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

@Mapper
public interface VaccineMapper {
	
	VaccineMapper INSTANCE = Mappers.getMapper(VaccineMapper.class);

	@Mapping(source="manufacturer",target="company")
	@Mapping(source="quantity",target="totalno")
	VaccineDTO fromVaccine(Vaccine vaccine);
}

What if there are nested objects in the source object ?

Let’s say your Vaccine entity class has a child object:

package com.example.demo;

public class Dosage {

	
	private int noOfDosesRequired;
	
	private int gapBetweenDoses;

	public int getNoOfDosesRequired() {
		return noOfDosesRequired;
	}

	public void setNoOfDosesRequired(int noOfDosesRequired) {
		this.noOfDosesRequired = noOfDosesRequired;
	}

	public int getGapBetweenDoses() {
		return gapBetweenDoses;
	}

	public void setGapBetweenDoses(int gapBetweenDoses) {
		this.gapBetweenDoses = gapBetweenDoses;
	}
	
	
}

Vaccine class now becomes:

package com.example.demo;

public class Vaccine {

	private String name;
	
	private String manufacturer;
	
	private int quantity;
	
	private Dosage dosage;
	

	public Dosage getDosage() {
		return dosage;
	}

	public void setDosage(Dosage dosage) {
		this.dosage = dosage;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getManufacturer() {
		return manufacturer;
	}

	public void setManufacturer(String manufacturer) {
		this.manufacturer = manufacturer;
	}

	public int getQuantity() {
		return quantity;
	}

	public void setQuantity(int quantity) {
		this.quantity = quantity;
	}
	
	
}

And you want to map these new properties to totalDoses and dosageGap in VaccineDTO object:

package com.example.demo;

public class VaccineDTO {

	private String name;
	
	private String company;
	
	private String totalno;
	
	private String totalDoses;
	
	private String dosageGap;
	

	public String getTotalno() {
		return totalno;
	}

	public void setTotalno(String totalno) {
		this.totalno = totalno;
	}

	public String getTotalDoses() {
		return totalDoses;
	}

	public void setTotalDoses(String totalDoses) {
		this.totalDoses = totalDoses;
	}

	public String getDosageGap() {
		return dosageGap;
	}

	public void setDosageGap(String dosageGap) {
		this.dosageGap = dosageGap;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	

	public String getCompany() {
		return company;
	}

	public void setCompany(String company) {
		this.company = company;
	}

	
	
}

This can be achieved using dot operator as below:

package com.example.demo;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

@Mapper
public interface VaccineMapper {
	
	VaccineMapper INSTANCE = Mappers.getMapper(VaccineMapper.class);

	@Mapping(source="manufacturer",target="company")
	@Mapping(source="quantity",target="totalno")
	@Mapping(source="dosage.noOfDosesRequired",target="totalDoses")
	@Mapping(source="dosage.gapBetweenDoses",target="dosageGap")
	VaccineDTO fromVaccine(Vaccine vaccine);
}

Here is the updated test controller class:

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestCotroller {

	
	@GetMapping("/test")
	public VaccineDTO test() {
		
		
		//populated here for demo but assume this is fetched from db:
		Vaccine vaccine = new Vaccine();
		vaccine.setName("Covaxin");
		vaccine.setManufacturer("Adar and Co");
		vaccine.setQuantity(100);
		Dosage dosage = new Dosage();
		dosage.setNoOfDosesRequired(2);
		dosage.setGapBetweenDoses(5);
		vaccine.setDosage(dosage);
		
		VaccineMapper mapper = VaccineMapper.INSTANCE;
		
		
		VaccineDTO dto = mapper.fromVaccine(vaccine);
		
		
		return dto;
		
	}
}

Run mvn clean install again and test it:

It worked!

Finally , lets see how to populate an already populated java bean with new values from another java bean (update)

There will be situations where you need to populate your DTO object with values from two or multiple entity objects.

In this case you first need to populate the values from the first entity bean as we had done in the previous steps .

Then you need to update the values from the second entity using @MappingTarget annotation.

Let’s see with example.

Let’s create a new entity class named Location like below :

package com.example.demo;

public class Location {

	private String country;

	private String city;

	private String state;

	public String getCountry() {
		return country;
	}

	public void setCountry(String country) {
		this.country = country;
	}

	public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}

	public String getState() {
		return state;
	}

	public void setState(String state) {
		this.state = state;
	}

}

And update the VaccineDTO class as below:

package com.example.demo;

public class VaccineDTO {

	private String name;
	
	private String company;
	
	private String totalno;
	
	private String totalDoses;
	
	private String dosageGap;
	
	private String country;
	
	private String state;
	
	private String city;
	
	

	public String getCountry() {
		return country;
	}

	public void setCountry(String country) {
		this.country = country;
	}

	public String getState() {
		return state;
	}

	public void setState(String state) {
		this.state = state;
	}

	public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}

	public String getTotalno() {
		return totalno;
	}

	public void setTotalno(String totalno) {
		this.totalno = totalno;
	}

	public String getTotalDoses() {
		return totalDoses;
	}

	public void setTotalDoses(String totalDoses) {
		this.totalDoses = totalDoses;
	}

	public String getDosageGap() {
		return dosageGap;
	}

	public void setDosageGap(String dosageGap) {
		this.dosageGap = dosageGap;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	

	public String getCompany() {
		return company;
	}

	public void setCompany(String company) {
		this.company = company;
	}

	
	
}

Now to update the values from Location object to VaccineDTO object create a new method in the mapper interface:

	void updateLocation(Location location, @MappingTarget VaccineDTO vaccineDTO);

The return type should be void and the mapping target should be the object which needs to be updated.

package com.example.demo;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;

@Mapper
public interface VaccineMapper {
	
	VaccineMapper INSTANCE = Mappers.getMapper(VaccineMapper.class);

	@Mapping(source="manufacturer",target="company")
	@Mapping(source="quantity",target="totalno")
	@Mapping(source="dosage.noOfDosesRequired",target="totalDoses")
	@Mapping(source="dosage.gapBetweenDoses",target="dosageGap")
	VaccineDTO fromVaccine(Vaccine vaccine);
	
	
	void updateLocation(Location location, @MappingTarget VaccineDTO vaccineDTO);
}

Let’s test it:

Here is the updated test controller:

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestCotroller {

	
	@GetMapping("/test")
	public VaccineDTO test() {
		
		
		//populated here for demo but assume this is fetched from db:
		Vaccine vaccine = new Vaccine();
		vaccine.setName("Covaxin");
		vaccine.setManufacturer("Adar and Co");
		vaccine.setQuantity(100);
		Dosage dosage = new Dosage();
		dosage.setNoOfDosesRequired(2);
		dosage.setGapBetweenDoses(5);
		vaccine.setDosage(dosage);
		
	
		
		VaccineMapper mapper = VaccineMapper.INSTANCE;
		
		
		VaccineDTO dto = mapper.fromVaccine(vaccine);
		
		Location location = new Location();
		location.setCity("Chennai");
		location.setCountry("India");
		location.setState("Tamilnadu");
		
		
		mapper.updateLocation(location, dto);
		
		
		return dto;
		
	}
}

Run mvn clean install again and restart the application.

Here is the output:

That’s it!

We will see how to do custom type conversions and use lombok along with MapStruct to remove the getters and setters in the code in the next post.

Here is the link to github:

https://github.com/vijaysrj/mapstructdemo


Posted

in

,

by

Comments

Leave a Reply

Discover more from The Full Stack Developer

Subscribe now to keep reading and get access to the full archive.

Continue reading