Search code examples
javaenumsjacksondeserializationfasterxml

In fasterxml, after deserialization json, if enum is first property in class, other fields are null


In fasterxml, after deserialization json, if enum (with JsonFormat.Shape.OBJECT) is first property in class, other fields are null.

Why enum should be last declared property in class to deserialize other fields properly?

Maybe this could be a bug in fasterxml?

Example class MyClass:

public class MyClass {

// >>>
// >>> element field is null after deserialization
// >>>
private MyEnum option; // first
private String element; // --> null

// >>> 
// >>> correctly deserialized if enum is last in order
// >>>
// private String element; // --> "elem"
// private MyEnum option; // last


public MyEnum getOption() {
    return option;
}

public void setOption(MyEnum option) {
    this.option = option;
}

public String getElement() {
    return element;
}

public void setElement(String element) {
    this.element = element;
}
} 

Example enum MyEnum:

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum MyEnum {

FIRST;

@JsonProperty
public String getOption() {
    return name();
}

@JsonCreator
public static MyEnum forValue(String option) {
    return FIRST;
}
}

Example main test class Main:

    public class Main {
public static void main(String[] args) throws IOException {
    ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    MyClass myClass = new MyClass();

    myClass.setElement("elem");
    myClass.setOption(MyEnum.FIRST);

    String serialized = mapper.writer().withDefaultPrettyPrinter().writeValueAsString(myClass);
    System.out.println(String.format("serialized - %s", serialized));

    MyClass deserialized = mapper.readValue(serialized, MyClass.class);

    String deserializedResult = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(deserialized);
    System.out.println(String.format("deserialized - %s", deserializedResult));
}
}

Output showing field is null after deserialization:

serialized - {
  "option" : {
   "option" : "FIRST"
  },
  "element" : "elem"
}
deserialized - {
  "option" : {
    "option" : "FIRST"
  },
  "element" : null
}

Output after fixing order (uncommented lines in MyClass):

serialized - {
  "element" : "elem",
  "option" : {
    "option" : "FIRST"
  }
}
deserialized - {
  "element" : "elem",
  "option" : {
    "option" : "FIRST"
  }
}

Solution

  • I couldn't tell you if it's a bug, you can debug and step through the code to understand how Jackson "fails" in this scenario. Your use of FAIL_ON_UNKNOWN_PROPERTIES hides the problem, which is using String as the parameter type of your forValue factory method. In short, Jackson gets "stuck" in traversing the tokens of the JSON content.

    To fix it properly, ie. not rely on order, you have a couple of options. First, get rid of the JsonFormat.Shape.OBJECT shape for serializing the enum type and its corresponding @JsonCreator. The default serialization/deserialization for an enum is to use its name anyway.

    Second, if you really want to keep the OBJECT shape, you'll need to change your @JsonCreator method to receive an ObjectNode, since that's what the JSON contains, not a String. From there, you can perform the deserialization yourself (assuming you have more enum constants)

    @JsonCreator
    public static MyEnum forValue(ObjectNode object) {
        return MyEnum.valueOf(object.get("option").asText());
    }