26

I have a JSON Object something like:

{"name":"John", "grade":"A"}

or

{"name":"Mike", "grade":"B"}

or

{"name":"Simon", "grade":"C"}

etc

I am trying to map the above JSON to:

@JsonIgnoreProperties(ignoreUnknown = true)
public class Employee{
      @JsonIgnoreProperties(ignoreUnknown = true)
      public enum Grade{ A, B, C }
      Grade grade;
      String name;

  public Grade getGrade() {
    return grade;
  }

  public void setGrade(Grade grade) {
    this.grade = grade;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

the above mapping works fine but in the future there will be more "Grade" types let say D,E etc which breaks the existing mapping and throws the following exception

05-08 09:56:28.130: W/System.err(21309): org.codehaus.jackson.map.JsonMappingException: Can not construct instance of Employee from String value 'D': value not one of declared Enum instance names

Is there a way to ignore unknown fields with in enum types?

Thanks

Saqib
  • 1,737
  • 2
  • 19
  • 30
  • 2
    Annotation `@JsonIgnoreProperties` does not work the way you expect for `Enum` types; it only (currently) allows ignoring unknown POJO properties. But I think this makes sense as an improvement idea. So could you file an Issue at [Jackson databind](https://github.com/FasterXML/jackson-databind) project? If so, maybe it could be added in Jackson 2.3? – StaxMan May 08 '13 at 20:35
  • 13
    The answers are a bit verbose. For those looking for quickest & simplest solution: set `READ_UNKNOWN_ENUM_VALUES_AS_NULL` [deserialization feature](https://github.com/FasterXML/jackson-databind/wiki/Deserialization-Features#value-conversions) to true. – Jonik Feb 05 '15 at 16:11
  • 1
    `mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);` – Matt Mar 02 '20 at 14:55

5 Answers5

30

I have found a way to do this like follows:

public static void main(String[] args) throws JsonParseException, JsonMappingException, UnsupportedEncodingException, IOException {
    String json = "{\"name\":\"John\", \"grade\":\"D\"}";

    ObjectMapper mapper = new ObjectMapper();
    mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
    Employee employee = mapper.readValue(new ByteArrayInputStream(json.getBytes("UTF-8")), Employee.class);

    System.out.println(employee.getGrade());
}

This outputs :

null

other classes:

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class Employee {
    private String name;
    private Grade grade;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }
}



import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public enum Grade {A, B, C}

I haven't come across a way to do this with an annotation yet.

I hope this helps.

stefaan dutry
  • 1,096
  • 1
  • 10
  • 21
  • @stefaan: This answer worked for me! All I was missing was the DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, great find! – Arthulia Mar 04 '15 at 17:37
  • 2
    As of Jackson 2.8, there is also READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, which lets you set a default value instead of null. https://fasterxml.github.io/jackson-databind/javadoc/2.8/com/fasterxml/jackson/databind/DeserializationFeature.html#READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE – Rick Jan 25 '18 at 15:21
22

I think you should define external deserializer for Grade enum.

I added additional field to enum - UNKNOWN:

enum Grade {
    A, B, C, UNKNOWN;

    public static Grade fromString(String value) {
        for (Grade grade : values()) {
            if (grade.name().equalsIgnoreCase(value)) {
                return grade;
            }
        }

        return UNKNOWN;
    }
}
class Employee {

    @JsonDeserialize(using = GradeDeserializer.class)
    private Grade grade;
    private String name;

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Employee [grade=" + grade + ", name=" + name + "]";
    }
}

Now, parser could look like that:

class GradeDeserializer extends JsonDeserializer<Grade> {
    @Override
    public Grade deserialize(JsonParser parser, DeserializationContext context)
            throws IOException, JsonProcessingException {
        return Grade.fromString(parser.getValueAsString());
    }
}

Example usage:

public class JacksonProgram {

    public static void main(String[] args) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();
        JsonFactory jsonFactory = new JsonFactory();
        JsonParser parser = jsonFactory
                .createJsonParser("{\"name\":\"John\", \"grade\":\"D\"}");
        Employee employee = objectMapper.readValue(parser, Employee.class);
        System.out.println(employee);
    }

}

Output:

Employee [grade=UNKNOWN, name=John]

If you don't want to add additional field, you would return null for example.

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • 1
    Having an UNKNOWN instance of the enum seems a poor choice to me. I'd prefer Stefaan's solution with the READ_UNKNOWN_ENUM_VALUES_AS_NULL feature, which will produce null. – Tom May 08 '13 at 20:27
  • 3
    UNKNOWN means that value is not known at this moment. null means - value is not set. It can be confusing in some situations and could cause NPE. Also, if enum values contain methods, we can implement special null-handling in more elegant way. For me solution with the READ_UNKNOWN_ENUM_VALUES_AS_NULL feature is good too. Maybe, is better than my... – Michał Ziober May 08 '13 at 22:33
  • 1
    Great solution if you don't want unknown values to return null. Thanks for the test case example too! – CaptRespect Mar 05 '15 at 15:13
  • 1
    Excellent! Thanks! – cristianmiranda Apr 11 '17 at 13:27
  • 1
    Note that as of Jackson 2.8, there is a READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE feature as well as READ_UNKNOWN_ENUM_VALUES_AS_NULL which lets you do with an annotation what the above code accomplishes. – evnafets Jul 28 '20 at 20:58
13

@JsonCreator provides a more concise solution compared to @JsonDeserialize.

The idea is to annotate your valueOf() replacement ( called safeValueOf() in this example) with @JsonCreator and then Jackson would deserialize strings using your implementation.

Note that the implementation is inside the enum, you can use it as field in other objects with no change.

The solution below is wrapped in a unit test so you can run it directly.

import static junit.framework.TestCase.assertEquals;

import java.io.IOException;

import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.databind.ObjectMapper;

public class EmployeeGradeTest {

    public enum Grade {
        A, B, C, OTHER;

        @JsonCreator
        public static Grade safeValueOf(String string) {
            try {
                return Grade.valueOf(string);
            } catch (IllegalArgumentException e) {
                return OTHER;
            }
        }
    }

    @Test
    public void deserialize() throws IOException {
        assertEquals(Grade.A, new ObjectMapper().readValue("\"A\"", Grade.class));
    }

    @Test
    public void deserializeNewValue() throws IOException {
        assertEquals(Grade.OTHER, new ObjectMapper().readValue("\"D\"", Grade.class));
    }
}
Bogdan Calmac
  • 7,993
  • 6
  • 51
  • 64
  • 1
    IMO this is the best solution, as it requires minimal changes: only the Enum type itself. The logic can be extracted to a static helper and easily reused on select enum types, where you need this kind of tolerance. The downside of the `READ_UNKNOWN_ENUM_VALUES_AS_NULL` option is that it's an all-or-nothing proposition. You must be ready to handle every single Jackson-referenced `enum` type suddenly silently deserializing as `null` instead of throwing an exception. – Astral Oct 17 '18 at 04:17
7

I am using boot2 (although this make work in boot 1 too) but you can just enable the deserialization feature in the applicaiton properties/yaml;

spring:
  jackson:
    deserialization:
      READ_UNKNOWN_ENUM_VALUES_AS_NULL: true
Jesse Nelson
  • 776
  • 8
  • 21
  • if you aren't using yaml: spring.jackson.deserialization.read-unknown-enum-values-as-null = true – spetz83 Aug 17 '21 at 15:13
3

There are two ways to handle such cases:

Option 1: Replace the unknown enum value with null

For this enable READ_UNKNOWN_ENUM_VALUES_AS_NULL setting in your ObjectMapper. After this, when you parse an Object with unknown enum field value it will be deserialised to null. Refer below for sample code:

import com.fasterxml.jackson.databind.*;

public static ObjectMapper getMapper() {
    return ObjectMapper().enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
}

Option 2: Replace the unknown enum value with a default enum value (say UNKNOWN)

For this, first enable READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE setting in your ObjectMapper. Then, in your enum class annotate the element which you want to be used as default enum value with @JsonEnumDefaultValue. Refer below for code sample:

import com.fasterxml.jackson.databind.*;

public static ObjectMapper getMapper() {
    return ObjectMapper().enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);
}


public enum YourEnum {
   A,
   B,
   C,
   @JsonEnumDefaultValue UNKNOWN;

}
Sahil Chhabra
  • 10,621
  • 4
  • 63
  • 62
  • 1
    This is probably the best answer out here. It presents two different possible solutions and both are as elegant as possible. Give it some love! – dominikbrandon Nov 17 '20 at 12:31