Java Enum Serialization / Deserialization with Jackson API

Jackson is the de-facto standard in the Java world for working with JSONs. JSON,  being the widely accepted and appreciated format for data transfer across different web services, most of the web service providers and consumers in the Java platform tend to use the Jackson API. Here are few of the use cases to serialize and deserialize Enums.

For analysis, the below object is used in all examples. The containing enums are defined as and when considered. @JsonInclude annotation used here simplifies the serialized JSON by ignoring the null values.

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public static class ObjectWithEnum
{
 private int someInteger = 100;
 private SimpleEnum simpleEnum;
 private WithIntField withIntField;
 private WithStringField withStringField;
 private WithMultiFields withMultiFields;
 private PrimaryColor primaryColor;
}

ObjectMapper, initialized as below is used in all examples:

ObjectMapper objectMapper = new ObjectMapper();

1. Simple Enum

Firstly, consider an enum which does not have any fields or methods as given below:

public enum SimpleEnum
{
 GOOD, BAD;
}

These enum  values are by default serialized into their respective names by invoking Enum.name().

ObjectWithEnum objectWithEnum = ObjectWithEnum.builder()
 .simpleEnum(SimpleEnum.GOOD).someInteger(100).build();
String jsonStr = objectWriter.writeValueAsString(objectWithEnum);
System.out.println(jsonStr);

Above code produces the output as below:

{
 "someInteger" : 100,
 "withOrdinal" : "GOOD"
}

If we need to produce the JSON where if ordinal (or index – GOOD=0, BAD=1) is used instead of name, then the below code can be used.

String jsonStr = objectMapper.writer(SerializationFeature.WRITE_ENUMS_USING_INDEX)
 .withDefaultPrettyPrinter().writeValueAsString(objectWithEnum);
System.out.println(jsonStr);

Below would be the output:

{
 "someInteger" : 100,
 "withOrdinal" : 0
}

Deserialization in both the above cases can be done with the below code:

objectWithEnum = objectMapper.readValue(jsonStr, ObjectWithEnum.class);

The second result of serialization above, by using the ordinal() by default instead of name() can also be achieved by using @JsonFormat annotation as give below. In this case, there is no need to provide the WRITE_ENUMS_USING_INDEX feature.

@JsonFormat(shape = JsonFormat.Shape.NUMBER)
public enum SimpleEnum {
 GOOD, BAD;
}

The same above behavior can even be achieved by using @JsonValue annotation as given below. Both of these work well with deserialization too.

public enum SimpleEnum {
 GOOD, BAD;
 @JsonValue
 public int value(){
  return ordinal();
 }
}

2. Enum with Integer Field

Consider the below enum which has an integer field. This enum has items which have name, ordinal and a field associated. As this has a defined field, it provides a possibility to implement toString() which can make use of the field and can be used in generating the JSON.

public enum WithIntField {
 ONE(1), TWO(2);

int i;

WithIntField(int i) {
 this.i = i;
 }

@Override
public String toString() {
 return "WithIntField{" +
 "i=" + i +
 "; name="+name()+"}";
}
}

The default approach, index based approach and using @JsonFormat would work the same way as SimpleEnum. As we have a toString() implemented, we have one more option to generate the JSON. And below would be the code to serialize and deserialize the JSON.

jsonStr = objectWriter.with(SerializationFeature.WRITE_ENUMS_USING_TO_STRING).writeValueAsString(objectWithEnum);
System.out.println(jsonStr);

objectWithEnum = objectMapper.reader(DeserializationFeature.READ_ENUMS_USING_TO_STRING).forType(ObjectWithEnum.class).readValue(jsonStr);
System.out.println(objectWithEnum);

The generated JSON would look like below.

{
 "someInteger" : 100,
 "withIntField" : "WithIntField2{i=2; name=TWO}"
}

The approach using @JsonValue to use the field value in JSON would not work as expected due to an existing bug with Jackson which the team does not seem to be interested to fix. To get this working, its required to adopt a workaround using @JsonCreator on a static method that converts the field into the enum value. The first annotation assists in the serialization and the second one in deserialization. The modified enum would look like below and ObjectMapper can be directly used for both marshalling and unmarshalling purposes. Few prefer to build a static Map  instead of Stream for performance benefits.

public enum WithIntField {
 ONE(1), TWO(2);

int i;

WithIntField(int i) {
 this.i = i;
 }

@JsonValue
 int toValue(){
 return i;
 }

@JsonCreator
 static WithIntField fromValue(int value){
 return Arrays.stream(WithIntField.values()).filter(e -> e.i == value).findFirst().get();
 }
}

3. Enum with multiple fields

Enums can have multiple fields and the below one is considered as example for demonstration.

public enum WithMultiFields {
 CAR("Long Trip", 4), BIKE("Short Trip", 2);

 String tripType;
 int wheelCount;

WithMultiFields(String tripType, int wheelCount) {
 this.tripType = tripType;
 this.wheelCount = wheelCount;
 }
 }

The default serialization and deserialization works by just representing the enum with its value on the JSON. Similar to the above case, if @JsonValue is used against the int field getter, it causes an error in deserialization. But works without any issues for String.

As this enum has multiple fields, it can even be represented as an object in the JSON like below:

{
 "someInteger" : 100,
 "withMultiFields" : {
 "wheelCount" : 2,
 "tripType" : "Short Trip"
 }
}

This would require us to add the @JsonFormat(shape = JsonFormat.Shape.OBJECT) annotation for the enum type and implement the getters for the fields required. Like other cases, this too fails during deserialization which can be worked around by providing a @JsonCreator static method. As there are multiple parameters for the method, each of the parameters have to be annotated with @JsonProperty annotation to instruct Jackson to provide appropriate values. The whole enum representation would be as represented below.

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum WithMultiFields {
 CAR("Long Trip", 4), BIKE("Short Trip", 2);
 String tripType;
 int wheelCount;

WithMultiFields(String tripType, int wheelCount) {
 this.tripType = tripType;
 this.wheelCount = wheelCount;
 }

public String getTripType() {
 return tripType;
 }

public int getWheelCount() {
 return wheelCount;
 }

@JsonCreator
 static WithMultiFields findValue(@JsonProperty("wheelCount") int wheelCount, @JsonProperty("tripType") String tripType){
 return Arrays.stream(WithMultiFields.values()).filter(v -> v.wheelCount == wheelCount && v.tripType.equals(tripType)).findFirst().get();
 }
}

4. Enum with POJO fields

Things might get a little complex when custom objects exist in the enum and the requirement is to represent the whole enum as an object. Consider the example below:

public enum PrimaryColor {
 RED("Rose", new Color((short) 255, (short) 0, (short) 0)), GREEN("Leaf", new Color((short) 0, (short) 255, (short) 0)), BLUE("Sky", new Color((short) 0, (short) 0, (short) 255));

String example;
 Color description;

PrimaryColor(String example, Color description) {
 this.example = example;
 this.description = description;
 }
}

public static class Color {
 short red;
 short green;
 short blue;
}

And the serialized JSON:

{
 "someInteger" : 100,
 "primaryColor" : {
 "name" : "BLUE",
 "example" : "Sky",
 "description" : {
 "red" : 0,
 "green" : 0,
 "blue" : 255
 }
 }
}

Serialization is achieved using @JsonFormat annotation on the enum with appropriate getters in both enum and the POJO. For deserialization, it would require a @JsonCreator static method on PrimaryColor and either a @JsonCreator static method or a default constructor on the Color. As our static method in PrimaryColor uses equals() method for comparison, even that needs to be implemented. @JsonPropertyOrder is meant to specify the order of members in the JSON. Finally, both the object definition is provided as below. Other annotations that are not discussed here are imported from the lombok library.

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
 @JsonPropertyOrder({"name", "example", "description"})
 public enum PrimaryColor {
 RED("Rose", new Color((short) 255, (short) 0, (short) 0)), GREEN("Leaf", new Color((short) 0, (short) 255, (short) 0)), BLUE("Sky", new Color((short) 0, (short) 0, (short) 255));

 String example;
 Color description;

PrimaryColor(String example, Color description) {
 this.example = example;
 this.description = description;
 }

public String getName(){
 return name();
 }

public String getExample() {
 return example;
 }

public Color getDescription() {
 return description;
 }

@JsonCreator
 static PrimaryColor findValue(@JsonProperty("example") String example, @JsonProperty("description") Color description){
 return Arrays.stream(PrimaryColor.values()).filter(v->v.example.equals(example) && v.description.equals(description)).findFirst().get();
 }

}



 @Getter
 @AllArgsConstructor
 @EqualsAndHashCode
 public static class Color {
 private short red;
 private short green;
 private short blue;

@JsonCreator
 static Color createColor(@JsonProperty("red") short red, @JsonProperty("green") short green, @JsonProperty("blue") short blue){
 return new Color(red, green, blue);
 }
 }

For handling any unusual or complex requirements, one can explore implementing custom serializers and deserializers and plug in to any class.

Note: The Jackson version considered here is 2.9.1 and on Java 1.8