1. Introduction

In this tutorial, we’ll learn how to map one enum type to another using MapStruct, mapping enums to built-in Java types such as int and String, and vice versa.

2. Maven

Let’s add the below dependency to our Maven pom.xml:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.6.0.Beta1</version> 
</dependency>

The latest stable release of MapStruct is available from the Maven Central Repository.

3. Mapping One Enum to Another

In this section, we’ll learn to perform various mappings.

3.1. Understanding the Use Case

Now, let us see a few real-world scenarios.

In REST API response mapping, MapStruct converts external API status codes into our application’s internal status enums.

For data transformation in microservices, MapStruct facilitates smooth data exchange between services by mapping similar enums.

Integration with third-party libraries often involves dealing with third-party enums. MapStruct simplifies this by converting them into our application’s enums.

3.2. Implementing the Mapping With MapStruct

To configure the mapping of source constant values to target constant values, we use the @ValueMapping MapStruct annotation. It maps based on names. However, we can also map a constant from the source enum to a constant with a different name in the target enum type. For instance, we can map a source enum “Go” to the target enum “Move“.

It’s also possible to map several constants from the source enum to the same constant in the target type.

TrafficSignal enum represents a traffic signal. The external service we interact with uses the RoadSign enum. The mapper would convert the enums to each other.

Let’s define the traffic signal enum:

public enum TrafficSignal {
    Off, Stop, Go
}

Let’s define the road sign enum:

public enum RoadSign {
    Off, Halt, Move
}

Let’s implement the @Mapper:

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

    @ValueMapping(target = "Off", source = "Off")
    @ValueMapping(target = "Go", source = "Move")
    @ValueMapping(target = "Stop", source = "Halt")
    TrafficSignal toTrafficSignal(RoadSign source);
}

@Mapper defines a MapStruct mapper called TrafficSignalMapper to convert enums to TrafficSignal. Its methods represent a mapping operation.

The @ValueMapping annotations within the interface specify explicit mappings between enum values. For example,  @ValueMapping(target = “Go”, source = “Move”) maps the Move enum to Go enum in TrafficSignal, and so on.

We need to make sure to map all enum values from source to target for complete coverage and prevent unexpected behavior.

Here’s the test for it:

@Test
void whenRoadSignIsMapped_thenGetTrafficSignal() {
    RoadSign source = RoadSign.Move;
    TrafficSignal target = TrafficSignalMapper.INSTANCE.toTrafficSignal(source);
    assertEquals(TrafficSignal.Go, target);
}

It verifies the mapping of RoadSign.Move to TrafficSignal.Go.

We must test mapping methods thoroughly with unit tests to ensure accurate behavior and detect potential issues.

4. Mapping String to Enum

Let’s convert text literal values into enum values.

4.1. Understanding the Use Case

Our application collects user input as strings. We map these strings to enum values to represent different commands or options. For example, we map “add” to Operation.ADD, “subtract” to Operation.SUBTRACT, etc.

We specify settings as strings in the application configuration. We map these strings to enum values to ensure type-safe configuration. For instance, we map “EXEC” to Mode.EXEC, “TEST” to Mode.TEST, etc.

We map external API strings to enum values in our application. For instance, we map “active” to Status.ACTIVE, “inactive” to Status.INACTIVE, etc.

4.2. Implementing the Mapping With MapStruct

Let’s use @ValueMapping to map each signal:

@ValueMapping(target = "Off", source = "Off")
@ValueMapping(target = "Go", source = "Move")
@ValueMapping(target = "Stop", source = "Halt")
TrafficSignal stringToTrafficSignal(String source);

Here’s the test for it:

@Test
void whenStringIsMapped_thenGetTrafficSignal() {
    String source = RoadSign.Move.name();
    TrafficSignal target = TrafficSignalMapper.INSTANCE.stringToTrafficSignal(source);
    assertEquals(TrafficSignal.Go, target);
}

It verifies “Move” maps to TrafficSignal.Go.

5. Handling Custom Name Transformation

Enum names may differ only by naming convention. It may follow a different case, prefix, or suffix convention. For example, a signal could be Go, go, GO, Go_Value, Value_Go.

5.1. Applying Suffix to Source Enum

We apply a suffix to the source enum to get the target enum. For instance, Go becomes Go_Value:

public enum TrafficSignalSuffixed { Off_Value, Stop_Value, Go_Value }

Let’s define the mapping:

@EnumMapping(nameTransformationStrategy = MappingConstants.SUFFIX_TRANSFORMATION, configuration = "_Value")
TrafficSignalSuffixed applySuffix(TrafficSignal source);

@EnumMapping defines custom mappings for enum types. nameTransformationStrategy specifies the transformation strategy to apply to the enum constant name before mapping. We pass the appropriate controlling value in the configuration.

Here is the test to check the suffix:

@ParameterizedTest
@CsvSource({"Off,Off_Value", "Go,Go_Value"})
void whenTrafficSignalIsMappedWithSuffix_thenGetTrafficSignalSuffixed(TrafficSignal source, TrafficSignalSuffixed expected) {
    TrafficSignalSuffixed result = TrafficSignalMapper.INSTANCE.applySuffix(source);
    assertEquals(expected, result);
}

5.2. Applying Prefix to Source Enum

We can also apply a prefix to the source enum to get the target enum. For instance, Go becomes Value_Go:

public enum TrafficSignalPrefixed { Value_Off, Value_Stop, Value_Go }

Let’s define the mapping:

@EnumMapping(nameTransformationStrategy = MappingConstants.PREFIX_TRANSFORMATION, configuration = "Value_")
TrafficSignalPrefixed applyPrefix(TrafficSignal source);

PREFIX_TRANSFORMATION tells MapStruct to apply the prefix “Value_” to the source enum.

Let’s check the prefix mapping:

@ParameterizedTest
@CsvSource({"Off,Value_Off", "Go,Value_Go"})
void whenTrafficSignalIsMappedWithPrefix_thenGetTrafficSignalPrefixed(TrafficSignal source, TrafficSignalPrefixed expected) {
    TrafficSignalPrefixed result = TrafficSignalMapper.INSTANCE.applyPrefix(source);
    assertEquals(expected, result);
}

5.3. Stripping Suffix From Source Enum

We remove suffixes from the source enum to get the target enum. For instance, Go_Value becomes Go.

Let’s define the mapping:

@EnumMapping(nameTransformationStrategy = MappingConstants.STRIP_SUFFIX_TRANSFORMATION, configuration = "_Value")
TrafficSignal stripSuffix(TrafficSignalSuffixed source);

STRIP_SUFFIX_TRANSFORMATION tells MapStruct to remove the suffix “_Value” from the source enum.

Here’s the test to check the stripped suffix:

@ParameterizedTest
@CsvSource({"Off_Value,Off", "Go_Value,Go"})
void whenTrafficSignalSuffixedMappedWithStripped_thenGetTrafficSignal(TrafficSignalSuffixed source, TrafficSignal expected) {
    TrafficSignal result = TrafficSignalMapper.INSTANCE.stripSuffix(source);
    assertEquals(expected, result);
}

5.4. Stripping Prefix From Source Enum

We remove a prefix from the source enum to get the target enum. For instance, Value_Go becomes Go.

Let’s define the mapping:

@EnumMapping(nameTransformationStrategy = MappingConstants.STRIP_PREFIX_TRANSFORMATION, configuration = "Value_")
TrafficSignal stripPrefix(TrafficSignalPrefixed source);

STRIP_PREFIX_TRANSFORMATION tells MapStruct to remove the prefix “Value_” from the source enum.

And here’s the test to check the stripped prefix:

@ParameterizedTest
@CsvSource({"Value_Off,Off", "Value_Stop,Stop"})
void whenTrafficSignalPrefixedMappedWithStripped_thenGetTrafficSignal(TrafficSignalPrefixed source, TrafficSignal expected) {
    TrafficSignal result = TrafficSignalMapper.INSTANCE.stripPrefix(source);
    assertEquals(expected, result);
}

5.5. Applying Lowercase to Source Enum

We apply lowercase to the source enum to get the target enum. For instance, Go becomes go:

public enum TrafficSignalLowercase { off, stop, go }

Let’s define the mapping:

@EnumMapping(nameTransformationStrategy = MappingConstants.CASE_TRANSFORMATION, configuration = "lower")
TrafficSignalLowercase applyLowercase(TrafficSignal source);

CASE_TRANSFORMATION and lower configuration tells MapStruct to apply lowercase to source enum.

Here is the test method to check for lowercase mapping:

@ParameterizedTest
@CsvSource({"Off,off", "Go,go"})
void whenTrafficSignalMappedWithLower_thenGetTrafficSignalLowercase(TrafficSignal source, TrafficSignalLowercase expected) {
    TrafficSignalLowercase result = TrafficSignalMapper.INSTANCE.applyLowercase(source);
    assertEquals(expected, result);
}

5.6. Applying Uppercase to Source Enum

We apply uppercase to the source enum to get the target enum. For instance, Mon becomes MON:

public enum TrafficSignalUppercase { OFF, STOP, GO }

Let’s define the mapping:

@EnumMapping(nameTransformationStrategy = MappingConstants.CASE_TRANSFORMATION, configuration = "upper")
TrafficSignalUppercase applyUppercase(TrafficSignal source);

CASE_TRANSFORMATION and upper configuration tell MapStruct to apply uppercase to the source enum.

Here is the test to verify uppercase mapping:

@ParameterizedTest
@CsvSource({"Off,OFF", "Go,GO"})
void whenTrafficSignalMappedWithUpper_thenGetTrafficSignalUppercase(TrafficSignal source, TrafficSignalUppercase expected) {
    TrafficSignalUppercase result = TrafficSignalMapper.INSTANCE.applyUppercase(source);
    assertEquals(expected, result);
}

5.7. Applying Capital Case to Source Enum

We apply the title case to the source enum to get the target enum. For instance, go becomes Go:

@EnumMapping(nameTransformationStrategy = MappingConstants.CASE_TRANSFORMATION, configuration = "captial")
TrafficSignal lowercaseToCapital(TrafficSignalLowercase source);

CASE_TRANSFORMATION and capital configuration tell MapStruct to capitalize the source enum.

Here is the test to check the capital case:

@ParameterizedTest
@CsvSource({"OFF_VALUE,Off_Value", "GO_VALUE,Go_Value"})
void whenTrafficSignalUnderscoreMappedWithCapital_thenGetStringCapital(TrafficSignalUnderscore source, String expected) {
    String result = TrafficSignalMapper.INSTANCE.underscoreToCapital(source);
    assertEquals(expected, result);
}

6. Additional Use Cases for Enum Mapping

There would be scenarios when we map the enum back to other types. Let’s look at them in this section.

6.1. Mapping Enum to String

Let’s define the mapping:

@ValueMapping(target = "Off", source = "Off")
@ValueMapping(target = "Go", source = "Go")
@ValueMapping(target = "Stop", source = "Stop")
String trafficSignalToString(TrafficSignal source);

The @ValueMapping maps enum values to strings. For instance, we map the Go enum to the “Go” string value, and so on.

Here is the test to check the string mapping:

@Test
void whenTrafficSignalIsMapped_thenGetString() {
    TrafficSignal source = TrafficSignal.Go;
    String targetTrafficSignalStr = TrafficSignalMapper.INSTANCE.trafficSignalToString(source);
    assertEquals("Go", targetTrafficSignalStr);
}

It verifies if the mapping maps enum TrafficSignal.Go to a string literal “Go”.

6.2. Mapping Enum to Integer or Other Numeric Types

Mapping directly to an integer can cause ambiguity due to multiple constructors. We add a default mapper method that converts the enum to an integer. In addition, we can also define a class with an integer property to address this issue.

Let’s define a wrapper class:

public class TrafficSignalNumber
{
    private Integer number;
    // getters and setters
}

Let’s map the enum to an integer using the default method:

@Mapping(target = "number", source = ".")
TrafficSignalNumber trafficSignalToTrafficSignalNumber(TrafficSignal source);

default Integer convertTrafficSignalToInteger(TrafficSignal source) {
    Integer result = null;
    switch (source) {
        case Off:
            result = 0;
            break;
        case Stop:
            result = 1;
            break;
        case Go:
            result = 2;
            break;
    }
    return result;
}

Here is the test to check the integer result:

@ParameterizedTest
@CsvSource({"Off,0", "Stop,1"})
void whenTrafficSignalIsMapped_thenGetInt(TrafficSignal source, int expected) {
    Integer targetTrafficSignalInt = TrafficSignalMapper.INSTANCE.convertTrafficSignalToInteger(source);
    TrafficSignalNumber targetTrafficSignalNumber = TrafficSignalMapper.INSTANCE.trafficSignalToTrafficSignalNumber(source);
    assertEquals(expected, targetTrafficSignalInt.intValue());
    assertEquals(expected, targetTrafficSignalNumber.getNumber().intValue());
}

7. Handling Unknown Enum Values

We need to handle unmatched enum values by setting defaults, handling nulls, or throwing exceptions based on the business logic. 

7.1. MapStruct Throws an Exception for Any Unmapped Properties

MapStruct raises an error if a source enum doesn’t have a corresponding one in the target type. In addition, MapStruct can also map remaining or unmapped values to a default.

We have two options applicable to source only: ANY_REMAINING and ANY_UNMAPPED. However, we need to use only one of these options at a time.

7.2. Mapping Remaining Properties

The ANY_REMAINING option maps any remaining source values with the same name to the default value.

Let’s define  a simple traffic signal:

public enum SimpleTrafficSignal { Off, On }

Notably, it has less number of values than TrafficSignal. However, MapStruct needs us to map all enum values.

Let’s define the mapping:

@ValueMapping(target = "Off", source = "Off")
@ValueMapping(target = "On", source = "Go")
@ValueMapping(target = "Off", source = "Stop")
SimpleTrafficSignal toSimpleTrafficSignal(TrafficSignal source);

We explicitly map to Off. It would be inconvenient to map these if there are many such values. We may miss mapping a few values. That is where ANY_REMAINING helps.

Let’s define the mapping:

@ValueMapping(target = "On", source = "Go")
@ValueMapping(target = "Off", source = MappingConstants.ANY_REMAINING)
SimpleTrafficSignal toSimpleTrafficSignalWithRemaining(TrafficSignal source);

Here, we map Go to On. And then using MappingConstants.ANY_REMAINING, we map any remaining value to Off. Isn’t that a cleaner implementation now?

Here is the test to check the remaining mapping:

@ParameterizedTest
@CsvSource({"Off,Off", "Go,On", "Stop,Off"})
void whenTrafficSignalIsMappedWithRemaining_thenGetTrafficSignal(TrafficSignal source, SimpleTrafficSignal expected) {
    SimpleTrafficSignal targetTrafficSignal = TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithRemaining(source);
    assertEquals(expected, targetTrafficSignal);
}

It verifies that all other values are mapped to Off except for the value Go.

7.3. Mapping Unmapped Properties

Instead of remaining values, we can instruct MapStruct to map the unmapped values irrespective of the name.

Let’s define the mapping:

@ValueMapping(target = "On", source = "Go")
@ValueMapping(target = "Off", source = MappingConstants.ANY_UNMAPPED)
SimpleTrafficSignal toSimpleTrafficSignalWithUnmapped(TrafficSignal source);

Here is the test to check unmapped mapping:

@ParameterizedTest
@CsvSource({"Off,Off", "Go,On", "Stop,Off"})
void whenTrafficSignalIsMappedWithUnmapped_thenGetTrafficSignal(TrafficSignal source, SimpleTrafficSignal expected) {
    SimpleTrafficSignal target = TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithUnmapped(source);
    assertEquals(expected, target);
}

It verifies that all other values are mapped to Off except for the value Go.

7.4. Handling Null Values

MapStruct can handle null sources and null targets using the NULL keyword.

Let’s suppose we need to map null input to OffGo to On, and any other unmapped value to null.

Let’s define the mapping:

@ValueMapping(target = "Off", source = MappingConstants.NULL)
@ValueMapping(target = "On", source = "Go")
@ValueMapping(target = MappingConstants.NULL, source = MappingConstants.ANY_UNMAPPED)
SimpleTrafficSignal toSimpleTrafficSignalWithNullHandling(TrafficSignal source);

We use MappingConstants.NULL to set the null value to the target. It’s also used to indicate null input.

Here is the test to check null mapping:

@CsvSource({",Off", "Go,On", "Stop,"})
void whenTrafficSignalIsMappedWithNull_thenGetTrafficSignal(TrafficSignal source, SimpleTrafficSignal expected) {
    SimpleTrafficSignal targetTrafficSignal = TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithNullHandling(source);
    assertEquals(expected, targetTrafficSignal);
}

7.5. Raising Exceptions

Let’s consider a scenario where we raise an exception instead of mapping it to a default or null.

Let’s define the mapping:

@ValueMapping(target = "On", source = "Go")
@ValueMapping(target = MappingConstants.THROW_EXCEPTION, source = MappingConstants.ANY_UNMAPPED)
@ValueMapping(target = MappingConstants.THROW_EXCEPTION, source = MappingConstants.NULL)
SimpleTrafficSignal toSimpleTrafficSignalWithExceptionHandling(TrafficSignal source);

We use MappingConstants.THROW_EXCEPTION to raise an exception for any unmapped input.

Here is the test to check the exception thrown:

@ParameterizedTest
@CsvSource({",", "Go,On", "Stop,"})
void whenTrafficSignalIsMappedWithException_thenGetTrafficSignal(TrafficSignal source, SimpleTrafficSignal expected) {
    if (source == TrafficSignal.Go) {
        SimpleTrafficSignal targetTrafficSignal = TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithExceptionHandling(source);
        assertEquals(expected, targetTrafficSignal);
    } else {
        Exception exception = assertThrows(IllegalArgumentException.class, () -> {
            TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithExceptionHandling(source);
        });
        assertEquals("Unexpected enum constant: " + source, exception.getMessage());
    }
}

It verifies that the result is an exception for Stop, or else it’s an expected signal.

8. Conclusion

In this article, we learned to map between enum types and other data types like strings or integers using MapStruct @ValueMapping. Whether mapping one enum to another or gracefully handling unknown enum values, @ValueMapping offers flexibility and strength in mapping tasks. By adhering to best practices and taking care of null inputs and unmatched values, we improve code clarity and maintainability.

As always, the source code for the examples is available over on GitHub.

Course – LS (cat=Java)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.