Search code examples
javajacksonjson-deserialization

How to handle deserialization of null and absent properties with Jackson?


There is a record class in which one of the properties is an enum field.

The results of deserialization differ from what I'm looking for. Obviously, when the field is null or absent Jackson does not use the @JsonCreator method and just inserts null.

I would like to avoid that "special treatment" and deserialize nulls (and field absence as well) the same way as other values.

Model classes

record MyRec(
  String first,
  Color second
){}

// ---

enum Color{
  RED, GREEN;

  @JsonCreator
  static Color fromName(String name){
    if (name == null || "red".equals(name)){
      return RED; 
    } else if ("green".equals(name)) {
      return GREEN;
    } else {
      throw new IllegalArgumentException(name);
    }
  }
}

Results of deserialization

        JSON                             actual            intent
------------------------------------|-------------------|------------------|------
 '{"first": "hi", "second": "red"}'   MyRec("hi", RED)    MyRec("hi", RED) |  ok
 '{"first": "hi", "second": null}'    MyRec("hi", null)   MyRec("hi", RED) |  ?
 '{"first": "hi"}'                    MyRec("hi", null)   MyRec("hi", RED) |  ?

How to force Jackson to use the @JsonCreator method for deserializing nulls and absent values?


Solution

  • You can define an overloaded constructor in your record delegating to the canonical constructor via call this(), and annotate it with @JsonCreator (otherwise, Jackson would use the canonical one).

    The attribute second can be received as a plain String and parsed using the method fromName() you've defined in the enum:

    record MyRec(String first, Color second) {
        @JsonCreator
        public MyRec(@JsonProperty("first") String first,
                     @JsonProperty("second") String second) {
    
            this(first, Color.fromName(second));
        }
    }
    

    Note: no data-binding annotations required in your enum.

    Usage example:

    public static void main(String[] args) throws JsonProcessingException {
        List<String> jsonRecords = List.of(
            """
                {"first": "hi", "second": "red"}""",
            """
                {"first": "hi", "second": null}""",
            """
                {"first": "hi"}"""
        );
        
        ObjectMapper mapper1 = new JsonMapper();
        List<MyRec> records = new ArrayList<>();
        
        for (String json : jsonRecords) {
            MyRec myRec = mapper1.readValue(json, MyRec.class);
            records.add(myRec);
        }
        
        records.forEach(System.out::println);
    }
    

    Output:

    MyRec[first=hi, second=RED]
    MyRec[first=hi, second=RED]
    MyRec[first=hi, second=RED]
    

    Sidenote: possibly you might want to use equalsIgnoreCase() instead of plain equals().*