1. Introduction

In this tutorial, we’ll explore the possibilities within Java for supplying an enum value from a constant to an annotation. To understand the main drivers of the proposed design decisions, we’ll start with the problem statement, followed by a demo use case.

After that, we’ll define the ideal solution, understand the Java language limitations, and finally go over some implementation options.

2. Problem Statement

Let’s imagine the following requirement. Within the controller class, both POST and PUT endpoints always need to have the same Content-Type.  Now, let’s see how we can share the same enum value in both endpoint definitions.

To better understand the problem statement, we’ll continue by exploring a demo use case.

3. Defining a Demo Use Case

To fulfill the requirement, we’ll need the following data structures.

A RequestContentType enum that looks like this:

enum RequestContentType {
    JSON, XML, HTML
}

Two custom annotations, @PutRequest and @PostRequest:

@interface PostRequest {
    RequestContentType type();
}
@interface PutRequest {
    RequestContentType type();
}

And finally, the following controller class:

class DataController {
    @PostRequest(contentType = RequestContentType.JSON)
    String createData() {
       // ...
    }

    @PutRequest(contentType = RequestContentType.JSON)
    public String updateData() {
        // ...
    }
}

As we can observe, the current controller implementation fulfills the requirement by referencing the JSON type two times for each function. Although this implementation fulfills the requirement, it is still not robust. Technically the @PostRequest can be easily initialized with a different contentType than the @PutRequest. 

In the next section, we’ll explore different approaches to achieving a strongly typed implementation that ensures that @PostRequest and @PutRequest always share the same contentType. We’ll define the ideal scenario, understand the Java language limitations, and finally explore the alternatives that we have.

4. Sharing the Same Enum Value

We want to ensure that by changing the RequestContentType in one central place, the change is reflected in all spots where the RequestContentType is referenced.

Next, we’ll look at what the usual way of thinking dictates to us.

4.1. Ideal Scenario

When we first think about this requirement, our mind flows in the following direction – let’s define a constant of type RequestContentType and then reference it in each annotation. Something that looks like this:

class DataController {
    static final RequestContentType REQUEST_TYPE = RequestContentType.JSON;

    @PostRequest(contentType = REQUEST_TYPE)
    String createData() {
        // ...
    }

    @PutRequest(contentType = REQUEST_TYPE)
    String updateData() {
        // ...
    }
}

This is the most straightforward and ideal implementation method. Unfortunately, it is not working as expected. This is because we face a compilation error stating, “Attribute value must be an enum constant”.

Next, let’s dig deeper to understand why this solution is not compiling and what constraints Java places on it.

4.2. Understanding the Java Constraints

As we can see in the JLS-9.7.1, in the case of annotations, if the element type is an enum, the only accepted value is an enum constant.  Following the same ubiquitous language of the Java Language specification, according to JLS-8.9.1, all enums, like JSON, XML, and HTML of the RequestContentType, are already constants. For more information about enums check a guide to Java Enums.

In conclusion, Java constrains us by design only directly to assign an enum to an annotation. So, the ideal scenario is not feasible.

5. Implementation Alternatives to Supply Enum as Constants to an Annotation

Now that we understand the limitations of the Java language let’s see how we can achieve the desired result. We’ll explore two options: simulating an enum by defining an interface with a set of integer constants and another using an enum with a nested static class inside. At the end, we’ll compare both approaches.

Next, let’s dive into both details and understand when to use one or the other.

5.1. Use an Enum Simulation With Integer Constants

Let’s start with the simulated enum, which looks like this:

interface SimulatedRequestContentType {
   static final int JSON = 1;
   static final int XML = 2;
   static final int HTML = 3;
}

Let’s also change the annotation definition like this to accept int types:

@interface PostRequest {
    int intContentType();
}

@interface PutRequest {
    int intContentType();
}

Finally, the usage would look something like this:

class DataController {
    static final int REQUEST_TYPE = SimulatedRequestContentType.JSON;

    @PostRequest(intContentType = REQUEST_TYPE)
    String createData() {
        // ...
    }

    @PutRequest(intContentType = REQUEST_TYPE)
    String updateData() {
        // ...
    }
}

As we see, this alternative solves the requirement but is no longer using enums.

Let’s see how we can still make use of enums.

5.2. Extend the Enum With a Nested Static Class for Constants

Now, let’s look at the option where we extend the initial enum with a nested static class to define the constants. The implementation of the enum ExtendedRequestContentType looks like this:

enum ExtendedRequestContentType {
    JSON(Constants.JSON_VALUE), XML(Constants.XML_VALUE), HTML(Constants.HTML_VALUE);

    ExtendedRequestContentType(String name) {
        if (!name.equals(this.name()))
        {
            throw new IllegalArgumentException();
        }
    }

    public static class Constants {
        public static final String JSON_VALUE = "JSON";
        public static final String XML_VALUE = "XML";
        public static final String HTML_VALUE = "HTML";
    }
}

The Constants class defines the values for each type. These will be used as parameter values of the annotations.

An important detail is the enum’s constructor that expects a string called name as a parameter. Basically, with this constructor, we ensure that whenever a new enum constant is defined, a constant with the same name will also be defined. Otherwise, an error while initializing the enum will be thrown. This will ensure a 1:1 mapping from the direction of the enum towards Constants.

Moreover, if we want to ensure a stricter bidirectional 1:1 mapping, we can write the following unit test:

@Test
public void testEnumAndConstantsSync() {
    Set<String> enumValues = getEnumNames();
    List<String> constantValues = getConstantValues();
    Set<String> uniqueConstantValues = constantValues.stream().distinct().collect(Collectors.toSet());
    assertEquals(constantValues.size(), uniqueConstantValues.size());
    assertEquals(enumValues, uniqueConstantValues);
}

In this unit test, we are first getting all the enum names as a Set. Then, using reflection, we get all the values of the public String constants. After, as a final step, we ensure that there are no constants with the same name and that there is an exactly 1:1 mapping between enums and constants.

Finally, let’s use the ExtendedRequestContentType:

class DataController {
    static final String EXTENDED_REQUEST_TYPE = ExtendedRequestContentType.Constants.XML_VALUE;

    @PostRequest(extendedContentType = EXTENDED_REQUEST_TYPE)
    String createData() {
        // ...
    }

    @PutRequest(extendedContentType = EXTENDED_REQUEST_TYPE)
    String updateData() {
        // ...
    }
}

5.3. Comparing the Alternatives

As we saw, both alternatives use data types other than enum to pass the value to the annotation. This is required to assign this value to another constant in the DataController class and share it between the annotations.

The main difference between the two is that in the option where we simulate an enum, we completely give up using enums, whereas, in the second option, we still keep using enums and ensure a 1:1 mapping to the defined constants.

The overhead of keeping enums and contents in sync makes total sense if we use enums and their functionality in other parts of the application. If we do so, then implementing a utility method to map from constant to enum value can be very useful:

static ExtendedRequestContentType toEnum(String constant) {
    return Arrays.stream(ExtendedRequestContentType.values())
      .filter(contentType -> contentType.name().equals(constant))
      .findFirst()
      .orElseThrow(IllegalArgumentException::new);
}

As a bottom line, if using enums is also required in other parts of the application, choose the second alternative; otherwise, exchange enums for constant values.

6. Conclusion

In this tutorial, we learned about Java’s limitations in supplying an enum value from a constant to an annotation and explored our alternatives. We started by looking at a use case where this requirement would be useful and then delving deeper into the language’s limitations. Finally, we implemented two different alternatives and explored their differences.

As always, the full implementation of this article can be found over on GitHub.