5
votes

I want to use Jackson JSON to serialize/deserialize a class containing an enum object. My class is:

class Bar {

    @JsonProperty("rateType")
    @JsonDeserialize(using = ReturnedRateTypeDeserializer.class)
    private ReturnedRateType rateType;

    public ReturnedRateType getRateType() {
        return rateType;
    }

    public void setRateType(ReturnedRateType rateType) {
        this.rateType = rateType;
    }
}

The enum class ReturnedRateType is defined as:

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ReturnedRateType {
    AA("AA"),
    BB("BB"),
    CC("CC");

    @JsonProperty("value")
    private String value;

    ReturnedRateType(String value) {
        this.value = value;
    }

    @JsonCreator
    public static ReturnedRateType fromValue(final String value) {
        if (value != null) {
            for (ReturnedRateType type : ReturnedRateType.values()) {
                if (value.equalsIgnoreCase(type.value)) {
                    return type;
                }
            }
        }
        return null;
    }
}

As you see, I added @JsonFormat annotation to tell Jackson to serialize this enum as POJO, and added @JsonCreator annotation to get a static factory method from given string to enum object. Since Jackson can only serialize but can't deserialize from object representation to enum, I added the following customized deserializer for the enum ReturnedRateType:

public class ReturnedRateTypeDeserializer extends JsonDeserializer<ReturnedRateType> {

    @Override
    public ReturnedRateType deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        ReturnedRateType type = ReturnedRateType.fromValue(jp.getValueAsString());
        if(type != null)
            return type;
        throw new JsonMappingException("invalid value for ReturnedRateType");
    }
} 

But when I tested deserialization from a JSON string to enum, I got the error. The JSON string is:

{"rateType": {"value": "AA"}}

My test code is:

@Test
public void RateTypeToEnum() {
    String json = "{\"rateType\": {\"value\": \"AA\"}}";
    System.out.println(json);
    ObjectMapper mapper = new ObjectMapper();
    Bar bar = null;
    try {
        bar = mapper.readValue(json, Bar.class);
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    System.out.println(bar.getRateType());
}

I expect to see the output should be AA. But jp.getValueAsString() in my customized deserializer ReturnedRateTypeDeserializer is null during the execution:

ReturnedRateType type = ReturnedRateType.fromValue(jp.getValueAsString());  //jp.getValueAsString() is null here!

Thus it returns error. So what is wrong here?

1
The problem may be as simple as your missing @JsonProperty("value") from the creator method. Without this, assumption is that incoming JSON data must be a single JSON String. Note that @JsonFormat does not have effect on custom (de)serializers or creators; it only changes behavior of default (de)serializers. So try adding that annotation in there first.StaxMan
I tried adding @JsonProperty("value") from the creator method but it still reports the same error. Actually I've put this annotation in the value field of the enum. The error seems to be related to jp.getValueAsString() returns null, which should return "AA" which is the string value of the given enum.tonga
Adding it to property is NOT enough, because pre-Java8, ctor/method names do NOT have name information -- without annotation, it will not be recognized as being related. So you must absolutely add it in constructor parameter (and may add to field if needed).StaxMan

1 Answers

9
votes

According to the Jackson 2.5.X documentation on the JsonFormat annotation the Shape.Object does not work for the enum deserialisation:

  • Enums: Shapes JsonFormat.Shape.STRING and JsonFormat.Shape.NUMBER can be used to change between numeric (index) and textual (name or toString()); but it is also possible to use JsonFormat.Shape.OBJECT to serialize (but not deserialize).

I'd make the JsonCreator static method accept a JsonNode and read the string value from it.

Note that this would work since 2.5.X. In early versions you would need to write a custom deserialiser. Here is an example:

public class JacksonEnumObjectShape {
    @JsonFormat(shape = JsonFormat.Shape.OBJECT)
    @JsonDeserialize(using = ReturnedRateTypeDeserializer.class)
    public enum ReturnedRateType {
        AA("AA"),
        BB("BB"),
        CC("CC");

        @JsonProperty("value")
        private String value;

        ReturnedRateType(String value) {
            this.value = value;
        }

        @JsonCreator
        public static ReturnedRateType fromValue(final JsonNode jsonNode) {

            for (ReturnedRateType type : ReturnedRateType.values()) {
                if (type.value.equals(jsonNode.get("value").asText())) {
                    return type;
                }
            }
            return null;
        }
    }
    // can be avoided since 2.5
    public static class ReturnedRateTypeDeserializer extends JsonDeserializer<ReturnedRateType> {

        @Override
        public ReturnedRateType deserialize(
                final JsonParser jp,
                final DeserializationContext ctxt) throws IOException {
            final JsonNode jsonNode = jp.readValueAsTree();
            return ReturnedRateType.fromValue(jsonNode);
        }
    }

    public static void main(String[] args) throws IOException {
        final ObjectMapper mapper = new ObjectMapper();
        final String json = mapper.writeValueAsString(ReturnedRateType.AA);
        System.out.println(json);
        System.out.println(mapper.readValue(json, ReturnedRateType.class));
    }
}

Output:

{"value":"AA"}
AA