1. Overview

Enumerations, or enums, are a powerful and widely used feature in the Java programming language. In certain scenarios, we may need to convert one enum type to another. This requirement could arise when we are integrating different libraries or frameworks, utilizing microservices from different platforms, or working with legacy code that is challenging to update.

In this article, we’ll explore different techniques for mapping or converting one enum to another in Java. We’ll examine both built-in mechanisms and external libraries that can assist.

2. Defining the Model

When it comes to converting enums, we can encounter two main cases where different implementation techniques can be used. The first case involves unrelated enums with different sets of values. The second case involves enums with the same values but representing different classes from a Java perspective. We cannot simply cast instances of such classes and still need to perform a mapping.

To illustrate these techniques, let’s define two data models. The first model represents a scenario where the enums have the same values:

public enum OrderStatus {
    PENDING, APPROVED, PACKED, DELIVERED;
}
public enum CmsOrderStatus {
    PENDING, APPROVED, PACKED, DELIVERED;
}

And the second model represents a scenario where the enums have different values:

public enum UserStatus {
    PENDING, ACTIVE, BLOCKED, INACTIVATED_BY_SYSTEM, DELETED;
}
public enum ExternalUserStatus {
    ACTIVE, INACTIVE
}

3. Using Java Core

Most enum conversions can be achieved using the core capabilities of the Java language without the need for external libraries.

3.1. Using Switch

One of the most straightforward options is to use the switch mechanism. By creating proper conditions for each enum constant, we can determine the corresponding converted value. The syntax of switch statements has evolved with different Java versions. Depending on the project’s Java version, the switch can be implemented differently.

Starting from Java 12, a new switch feature was introduced, including switch expressions and multiple case values. This allows us to directly return the result of the switch and combine multiple values into a single case. Java 12 has a preview version of this feature (we can use it, but additional configuration is needed), whereas a permanent version is available from Java 14.

Let’s see an implementation example:

public ExternalUserStatus toExternalUserStatusViaSwitchStatement() {
    return switch (this) {
        case PENDING, BLOCKED, INACTIVATED_BY_SYSTEM, DELETED -> ExternalUserStatus.INACTIVE;
        case ACTIVE -> ExternalUserStatus.ACTIVE;
    };
}

However, a plain switch statement can still be used with an older version of Java:

public ExternalUserStatus toExternalUserStatusViaRegularSwitch() {
    switch (this) {
    case PENDING:
    case BLOCKED:
    case INACTIVATED_BY_SYSTEM:
    case DELETED:
        return ExternalUserStatus.INACTIVE;
    case ACTIVE:
        return ExternalUserStatus.ACTIVE;
    }
    return null;
}

The test snippet below demonstrates how this works:

@Test
void whenUsingSwitchStatement_thenEnumConverted() {
    UserStatus userStatusDeleted = UserStatus.DELETED;
    UserStatus userStatusPending = UserStatus.PENDING;
    UserStatus userStatusActive = UserStatus.ACTIVE;

    assertEquals(ExternalUserStatus.INACTIVE, userStatusDeleted.toExternalUserStatusViaSwitchStatement());
    assertEquals(ExternalUserStatus.INACTIVE, userStatusPending.toExternalUserStatusViaSwitchStatement());
    assertEquals(ExternalUserStatus.ACTIVE, userStatusActive.toExternalUserStatusViaSwitchStatement());
}

@Test
void whenUsingSwitch_thenEnumConverted() {
    UserStatus userStatusDeleted = UserStatus.DELETED;
    UserStatus userStatusPending = UserStatus.PENDING;
    UserStatus userStatusActive = UserStatus.ACTIVE;

    assertEquals(ExternalUserStatus.INACTIVE, userStatusDeleted.toExternalUserStatusViaRegularSwitch());
    assertEquals(ExternalUserStatus.INACTIVE, userStatusPending.toExternalUserStatusViaRegularSwitch());
    assertEquals(ExternalUserStatus.ACTIVE, userStatusActive.toExternalUserStatusViaRegularSwitch());
}

It’s worth mentioning that this conversion logic doesn’t necessarily need to reside in the enum class itself, but it’s better to encapsulate the logic if possible.

3.2. Using Member Variables

Another way to convert enums is to leverage the possibility of defining fields inside enums. By specifying an externalUserStatus field in UserStatus and providing the desired value in the constant declaration, we can define an explicit mapping for each enum value. In this approach, the conversion method returns the externalUserStatus value.

The UserStatus definition will look like this:

public enum UserStatusWithFieldVariable {
    PENDING(ExternalUserStatus.INACTIVE),
    ACTIVE(ExternalUserStatus.ACTIVE),
    BLOCKED(ExternalUserStatus.INACTIVE),
    INACTIVATED_BY_SYSTEM(ExternalUserStatus.INACTIVE),
    DELETED(ExternalUserStatus.INACTIVE);

    private final ExternalUserStatus externalUserStatus;

    UserStatusWithFieldVariable(ExternalUserStatus externalUserStatus) {
        this.externalUserStatus = externalUserStatus;
    }
}

And conversion method will return the externalUserStatus value:

public ExternalUserStatus toExternalUserStatus() {
    return externalUserStatus;
}

3.3. Using EnumMap

The methods described above works well for all kind of enums that can be modified. However, in situations where we cannot update the source code or prefer to keep the conversion logic outside of the enum itself, the EnumMap class can be useful.

EnumMap will help us to make independent mapping:

public class UserStatusMapper {
    public static EnumMap<UserStatus, ExternalUserStatus> statusesMap;
    static {
        statusesMap = new EnumMap<>(UserStatus.class);
        statusesMap.put(UserStatus.PENDING, ExternalUserStatus.INACTIVE);
        statusesMap.put(UserStatus.BLOCKED, ExternalUserStatus.INACTIVE);
        statusesMap.put(UserStatus.DELETED, ExternalUserStatus.INACTIVE);
        statusesMap.put(UserStatus.INACTIVATED_BY_SYSTEM, ExternalUserStatus.INACTIVE);
        statusesMap.put(UserStatus.ACTIVE, ExternalUserStatus.ACTIVE);
    }
}

EnumMap is specifically optimized for use with enums as map keys, and it’s better to avoid using other types of maps.

3.4. Using Enum Name

When converting between enums with the same values, we can rely on the valueOf() method provided by Java. This method returns the enum value based on the provided enum name. However, it’s important to note that the provided name must exactly match the identifier used to declare the enum constant. If the provided name isn’t found among the declared constants, an IllegalArgumentException will be thrown.

It’s important to use the enum name approach with caution, especially when working with enums from external libraries or services that are beyond our control. Mismatches between the two enums can lead to runtime errors and can potentially break the service.

To show explained approach, we’ll use OrderStatus and CmsOrderStatus entities:

public CmsOrderStatus toCmsOrderStatus() {
    return CmsOrderStatus.valueOf(this.name());
}

3.5. Using Ordinal Approach

The ordinal approach is an interesting but tricky technique that relies on the internal implementation of enums. Internally, enums are represented as an array of constants, allowing us to iterate through the values and access them using indices.

Let’s see how we can use ordinal() functionality to convert OrderStatus to CmsOrderStatus:

public CmsOrderStatus toCmsOrderStatusOrdinal() {
    return CmsOrderStatus.values()[this.ordinal()];
}

And test shows the usage:

@Test
void whenUsingOrdinalApproach_thenEnumConverted() {
    OrderStatus orderStatusApproved = OrderStatus.APPROVED;
    OrderStatus orderStatusDelivered = OrderStatus.DELIVERED;
    OrderStatus orderStatusPending = OrderStatus.PENDING;

    assertEquals(CmsOrderStatus.APPROVED, orderStatusApproved.toCmsOrderStatusOrdinal());
    assertEquals(CmsOrderStatus.DELIVERED, orderStatusDelivered.toCmsOrderStatusOrdinal());
    assertEquals(CmsOrderStatus.PENDING, orderStatusPending.toCmsOrderStatusOrdinal());
}

It’s important to note that the ordinal approach has its limitations. It may not show any issues during compilation, but it’s prone to runtime failures when enums become incompatible in terms of sizes. Even if the sizes of the enums remain compatible, changes in their values or indexing can result in erroneous and inconsistent behavior.

Although the ordinal approach is suitable for certain scenarios, it’s generally advisable to use more stable methods. As stated in the Java documentation:

the ordinal approach is primarily designed for use in sophisticated enum-based data structures such as java.util.EnumSet and java.util.EnumMap.

4. Using MapStruct

MapStruct is a popular library used for mapping between entities, and it also can be useful for enum conversions.

4.1. Maven Dependency

Let’s add the MapStruct dependency to our pom.xml. We need the MapStruct library as a dependency and MapStruct Processor as an annotation processor:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version>
</dependency>
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.5.1</version>
    <configuration>
        <source>17</source>
        <target>17</target>
        <annotationProcessorPaths>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.5.5.Final</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

We can find the latest versions of MapStruct and its processor in the Maven Central repository*.*

4.2. Usage

MapStruct generates implementation based on annotations defined in an interface. We can use this lib in both cases when enum values are completely different, as well as in the case when values are the same. If no specific mapping is specified, MapStruct automatically tries to map based on a match between constants’ names. At the same time, we can configure the explicit mapping between values in our enums. With the newer MapStruct version, we should use @ValueMapping for enums instead of @Mapping.

Considering our model, mapping for OrderStatus and CmsOrderStatus can be defined without any manual mapping. Mapping between UserStatus and ExternalUserStatus requires additional configuration where enum names don’t match:

@Mapper
public interface EnumMapper {

    CmsOrderStatus map(OrderStatus orderStatus);

    @ValueMapping(source = "PENDING", target = "INACTIVE")
    @ValueMapping(source = "BLOCKED", target = "INACTIVE")
    @ValueMapping(source = "INACTIVATED_BY_SYSTEM", target = "INACTIVE")
    @ValueMapping(source = "DELETED", target = "INACTIVE")
    ExternalUserStatus map(UserStatus userStatus);
}

At the same time, mapping for UserStatus.ACTIVE and ExternalUserStatus.ACTIVE will be generated automatically.

Apart from that, we can utilize MappingConstants.ANY_REMAINING feature that will put default value (in our case ExternalUserStatus.INACTIVE) for an enum constant that is not yet mapped at the end of mapping:

@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "INACTIVE")
ExternalUserStatus mapDefault(UserStatus userStatus);

5. Best Practices and Use Cases

The choice of a suitable conversion strategy depends on the specific requirements of a project. Factors such as the similarity between enum constants, access to the source code, and how often the enums may change should be considered.

When we have access to the enum’s source code, the switch and member variables approaches are recommended. These approaches allow us to encapsulate the conversion logic in a single location, providing better code organization. Additionally, the switch approach can be extended to use some external properties and perform more complex mapping logic. However, it’s important to note that using the switch approach can lead to lengthy and difficult-to-manage code in scenarios with heavy conditions and huge enums.

When we don’t have access to the source code or want to clearly visualize the mapping without lengthy switch statements, the EnumMap approach is a good option. EnumMap allows us to define explicit mappings between enums without the need for extensive switch statements. It’s particularly useful when the conversion logic is straightforward.

The enum name approach should only be used when the source and target enum names are identical. In cases where there might be a name mismatch, it’s preferable to extend the mapping method by incorporating additional logic and defining default behavior. This ensures that the conversion can gracefully handle mismatches and unexpected situations.

The ordinal approach, while useful in scenarios where mapping logic is based on indexing, should generally be avoided due to its inherent risks. Both the enum name and ordinal approaches require a resilient implementation to account for changes in enum sizes or values.

Alternatively, to avoid maintaining complex switch statements or maps, MapStruct can be used to automate the process. It can handle both enums with identical names and those with completely different names, allowing us to define additional mapping logic and default behaviors.

Regardless of the chosen approach, it’s crucial to update the mappings as soon as the enums are changed. If updating the mappings is not feasible, ensure that the code is designed to handle enum changes appropriately, either by falling back to default mapping values or by gracefully handling new enum values as they arise. This approach ensures the longevity and stability of enum conversions.

6. Conclusion

In this article, we explored various techniques for mapping enums in Java. We discussed both built-in mechanisms and the use of MapStruct. Depending on specific use cases and requirements, different techniques may be more suitable than others. It’s recommended to consider multiple factors when choosing a conversion strategy, such as the similarity between enum constants, access to source code, and the potential for enum changes.

The complete examples are available over on GitHub.