Jackson deserializing into Map with an Enum Key, POJO Value
Asked Answered
C

3

15

I am trying to deserialize JSON into a Java POJO using Jackson. Without giving away confidential information, here is an example stack trace when ObjectMapper's deserialization fails:

org.codehaus.jackson.map.JsonMappingException: Can not construct Map key of type com.example.MyEnum from String "coins": not a valid representation: Can not construct Map key of type com.example.MyEnum from String "coins": not one of values for Enum class

My JSON looks like this:

"foo": {
    "coins": null,
    ...
}

And the class I want to deserialize into has this field:

private Map<MyEnum, MyPojo> foo;

And my enum type looks like this:

public enum MyEnum {
    COINS("coins"),
    ...
}

I do realize that I am trying to deserialize a null value. But I believe this should still work: the result of the deserialization should be equivalent to having a Map with foo.put(MyEnum.COINS, null), which is indeed a valid Java instruction. Help is much appreciated, thanks in advance.

Cushat answered 15/11, 2012 at 21:30 Comment(2)
provide a static factory method in your enumeration class that constructs enum by stringBlackford
And the reason for issue is that by default Jackson uses enum.name() as the id -- and in this case, name is "COINS", not lowe-case "coins". There are ways around this, as answers point out.Pennell
P
14

In addition to one good solution presented (factory method), there are 2 other ways:

  • If MyEnum.toString() would return coins, you can make Jackson use toString() over name() with ObjectMapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
  • You could add some other method to return id to use, and mark that method with @JsonValue annotation (you can actually use that on toString() as well, instead of enabling the above feature) -- if that annotation exists, the value returned by that method is used as the id.
Pennell answered 16/11, 2012 at 0:53 Comment(1)
Keep in mind that ObjectMapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING) affects all the operations of the mapper, so it may throw unexpected results if you have different enums with custom toStrings, for instance.Camshaft
C
11

GRR! Figured it out.

Solution for me was to create a static method, annotated with @JsonCreator, in my enum that creates an instance of the enum, based on a String parameter. It looked like this:

@JsonCreator
public static MyEnum create(String str) {
    // code to return an enum based on the String parameter
}
Cushat answered 15/11, 2012 at 22:6 Comment(1)
just as a note, no instances of enum are created on the fly. Enums are singletons.Camshaft
B
7

Provide a static factory method in your enumeration class that constructs enum by string and annotate it with @JsonCreator:

@JsonCreator
public static MyEnum fromValue(String v) {
    for (MyEnum myEnum : values()) {
        if (myEnum.text.equals(v)) {
            return myEnum;
        }
    }
    throw new IllegalArgumentException("invalid string value passed: " + v);
}
Blackford answered 15/11, 2012 at 22:11 Comment(3)
wouldn't a return valueOf(v); be sufficient?Radiogram
@StefanHendriks, it would be sufficient if author reconstructed enum by enum name (COINS), but question says it is deserialized using enum custom field (coins)Blackford
@StefanHendriks in that case, a custom creator wouldn't be necessary because Jackson can handle that properly by default. Anyway, I would allways recomend ORing the condition, so it would also accept if matched name() perfectly, where it is possible. Other option is that if name is allways the uppercase of text, just toUpperCase v and perform a regular valueOf(v.toUpperCase())Camshaft

© 2022 - 2024 — McMap. All rights reserved.