My team has been working on updating a legacy product to make its UI more feature-rich, reliable, and easier to use. This product accepts different types of files, reads them, and sends the data to an API so that it can be ingested on a client’s behalf. It’s basically there for clients who aren’t technical enough to write API calls themselves, but do want to share data.
One feature that we wanted to define was what we called IngestionFormatType
. Clients can provide files in two possible formats: ndjson
and csv
. As we store rows in a table that contain configuration data for these ingestion jobs, it’s important to note the IngestionFormatType
, so we had to update a table to include this column, update the UI to allow users to make a selection, and update the back end to be able to process that selection.
I wrote a PR that passed around the format type as a String, but my manager called it out right away. He suggested, since there are only two possible options, that I make it an enum instead. Great, I said, now I just have to learn how to deal with whatever type changes come with the process of request -> enum -> DB.
Java Enum Syntax
Enum is short for “enumerated type,” which means “specifically listed.” Enums can be used any time we want to limit our options to a series of choices. In this case, those choises are file types: ndjson
and csv
. In Java we can create an enum just like this:
public enum IngestionFormatType {
NDJSON,
CSV;
}
If we want to reference the enum, we can import it and write IngestionFormatType.NDJSON
or IngestionFormatType.CSV
. But we know that we eventually want to write these values into a table in our DB. The ingestion_format_type
column accepts a string as its value, so we have to be able to change these values into strings. We can do that from within the class:
public enum IngestionFormatType {
NDJSON("ndjson"),
CSV("csv");
private final String dbIngestionFormatType;
IngestionFormatType(String dbIngestionFormatType) {
this.dbIngestionFormatType = dbIngestionFormatType;
}
public String getDbIngestionFormatType() {
return dbIngestionFormatType;
}
}
The first difference here is that the two elements in our list are now being passed arguments. Instead of NDJSON
, we see NDJSON("ndjson")
. This is because we’ve given our enum a constructor with a parameter. This parameter, a String, is the type we want to use for the database table. We add a public getter method so we can access it as we’re writing a SQL query, a log, or anything else that would require a String.
But there’s one more value we have to match. We’re sending data to the DB, but we’re receiving it from the front end via gRPC . The format of the gRPC call is generated by files that we write on the back end called protobufs. In this case, our protobuf defined the ingestion format like this:
enum FileJobIngestionFormat {
FILE_JOB_INGESTION_FORMAT_UNKNOWN = 0;
FILE_JOB_INGESTION_FORMAT_CSV = 1;
FILE_JOB_INGESTION_FORMAT_NDJSON = 2;
}
This is a completely different naming convention than what we’re using in the Java code and the DB, so it’s a good thing our enum class can be used as a translator:
public enum IngestionFormatType {
NDJSON(FILE_JOB_INGESTION_FORMAT_NDJSON, "ndjson"),
CSV(FILE_JOB_INGESTION_FORMAT_CSV, "csv");
private final ContractProtos.FileJobIngestionFormat fileJobIngestionFormat;
private final String dbIngestionFormatType;
IngestionFormatType(ContractProtos.FileJobIngestionFormat fileJobIngestionFormat, String dbIngestionFormatType) {
this.fileJobIngestionFormat = fileJobIngestionFormat;
this.dbIngestionFormatType = dbIngestionFormatType;
}
public ContractProtos.FileJobIngestionFormat getFileJobIngestionFormat() {
return fileJobIngestionFormat;
}
public String getDbIngestionFormatType() {
return dbIngestionFormatType;
}
}
Now our constructor accepts two arguments, one for the string version of our enum for the DB and one for the proto version of our enum for gRPC. No matter where this enum is referenced on the back end, we can translate it to any of the three values.
Reverse Translation
We now have an enum that can be converted to a string or a protobuf enum. But this doesn’t account for one very important use case: when we receive a gRPC call from the front end, the data is in a format defined by the protobuf. We can go from enum -> proto, but we can’t yet go from proto -> enum. Unless…
public static IngestionFormatType from(ContractProtos.FileJobIngestionFormat fileJobIngestionFormat) {
return Stream.of(values())
.filter(ingestionFormatTypeEnum -> ingestionFormatTypeEnum.getFileJobIngestionFormat().equals(fileJobIngestionFormat))
.findFirst()
.orElseThrow(() -> new IllegalStateException(
"FileJobIngestionFormat must be resolved to a proto enum. FileJobIngestionFormat given: "
+ fileJobIngestionFormat));
}
Here we define our from
method. It accepts a FileJobIngestionFormat
type from the ContractProtos
class and returns an IngestionFormatType
, which is the enum class we defined earlier. To do that, we have to create a stream of all the values from our enum class. The static values method gives us exactly what we want. Per the Oracle docs, enum types “have a static values method that returns an array containing all of the values of the enum in the order they are declared. This method is commonly used in combination with the for-each construct to iterate over the values of an enum type.”
We don’t use forEach, but instead use a stream so we can do everything in a single line. We use filter
to check each element and see if its fileJobIngestionFormat
equals the format we’re passing as an argument. We either return the first (and only, really) match or we return an exception because there were no matches and therefore some data is invalid. Tada — now we know the enum that corresponds to the proto! We can do the same thing to go from the string from the table to the enum:
public static IngestionFormatType fromString(String stringRepresentationOfEnum) {
return Stream.of(values())
.filter(IngestionFormatTypeEnum -> IngestionFormatTypeEnum.getDbIngestionFormatType().equals(stringRepresentationOfEnum))
.findFirst()
.orElseThrow(() ->
new IllegalStateException("String must be resolved to a IngestionFormatType. String given: "
+ stringRepresentationOfEnum));
}
Effort
My original PR, that just passed strings around, worked just fine. And it took me 1–2 extra days plus help from other engineers to figure out all this enum mess. But the effort was worth it because:
- The code is now more clear, safe, and sustainable
- I learned something new that won’t take me as long in the future.
These likely aren’t lessons that my readers need to learn, but they’re important for me to acknowledge. I love taking the easy path to anything. Hard work is hard! Remembering the benefits of extra effort will likely make us happier in the long run and I hope that I and all the engineers working on this project get to enjoy these enums.
Sources
- Java enum Constructor, Programiz
- gRPC Docs
- Interface Stream, Oracle
- Enum Types, Oracle