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.

ecbrodie
  • 11,246
  • 21
  • 71
  • 120
  • 1
    provide a static factory method in your enumeration class that constructs enum by string – hoaz Nov 15 '12 at 22:08
  • 1
    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. – StaxMan Nov 16 '12 at 00:50

3 Answers3

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.
Saikat
  • 14,222
  • 20
  • 104
  • 125
StaxMan
  • 113,358
  • 34
  • 211
  • 239
  • 2
    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. – DGoiko Nov 30 '22 at 11:38
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
}
ecbrodie
  • 11,246
  • 21
  • 71
  • 120
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);
}
hoaz
  • 9,883
  • 4
  • 42
  • 53
  • 1
    wouldn't a `return valueOf(v);` be sufficient? – Stefan Hendriks Jan 29 '21 at 09:47
  • @StefanHendriks, it would be sufficient if author reconstructed enum by enum name (COINS), but question says it is deserialized using enum custom field (coins) – hoaz May 25 '21 at 23:40
  • @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()) – DGoiko Nov 30 '22 at 11:34