92

I'm trying and failing to deserialize an enum with Jackson 2.5.4, and I don't quite see my case out there. My input strings are camel case, and I want to simply map to standard Enum conventions.

@JsonFormat(shape = JsonFormat.Shape.STRING)
public enum Status {
    READY("ready"),
    NOT_READY("notReady"),
    NOT_READY_AT_ALL("notReadyAtAll");

    private static Map<String, Status> FORMAT_MAP = Stream
            .of(Status.values())
            .collect(toMap(s -> s.formatted, Function.<Status>identity()));

    private final String formatted;

    Status(String formatted) {
        this.formatted = formatted;
    }

    @JsonCreator
    public Status fromString(String string) {
        Status status = FORMAT_MAP.get(string);
        if (status == null) {
            throw new IllegalArgumentException(string + " has no corresponding value");
        }
        return status;
    }
}

I've also tried @JsonValue on a getter to no avail, which was an option I saw reported elsewhere. They all blow up with:

com.fasterxml.jackson.databind.exc.InvalidFormatException: Can not construct instance of ...Status from String value 'ready': value not one of declared Enum instance names: ...

What am I doing wrong?

jwilner
  • 6,348
  • 6
  • 35
  • 47
  • @FedericoPeraltaSchaffner, I wish this were true, but it certainly still blows up -- I just checked. I think it can't deal with the variety of cases. – jwilner Jul 29 '15 at 00:13
  • @FedericoPeraltaSchaffner: Same -- "value not one of the declared Enum instance names" – jwilner Jul 29 '15 at 00:16
  • How about if you try "READY"? – Simon Jul 29 '15 at 00:33
  • @Simon, sure it works then if all your JSON values match perfectly -- but I'm deserializing and don't have the luxury of changing the inputs! – jwilner Jul 29 '15 at 00:38

7 Answers7

169

EDIT: Starting from Jackson 2.6, you can use @JsonProperty on each element of the enum to specify its serialization/deserialization value (see here):

public enum Status {
    @JsonProperty("ready")
    READY,
    @JsonProperty("notReady")
    NOT_READY,
    @JsonProperty("notReadyAtAll")
    NOT_READY_AT_ALL;
}

(The rest of this answer is still valid for older versions of Jackson)

You should use @JsonCreator to annotate a static method that receives a String argument. That's what Jackson calls a factory method:

public enum Status {
    READY("ready"),
    NOT_READY("notReady"),
    NOT_READY_AT_ALL("notReadyAtAll");

    private static Map<String, Status> FORMAT_MAP = Stream
        .of(Status.values())
        .collect(Collectors.toMap(s -> s.formatted, Function.identity()));

    private final String formatted;

    Status(String formatted) {
        this.formatted = formatted;
    }

    @JsonCreator // This is the factory method and must be static
    public static Status fromString(String string) {
        return Optional
            .ofNullable(FORMAT_MAP.get(string))
            .orElseThrow(() -> new IllegalArgumentException(string));
    }
}

This is the test:

ObjectMapper mapper = new ObjectMapper();

Status s1 = mapper.readValue("\"ready\"", Status.class);
Status s2 = mapper.readValue("\"notReadyAtAll\"", Status.class);

System.out.println(s1); // READY
System.out.println(s2); // NOT_READY_AT_ALL

As the factory method expects a String, you have to use JSON valid syntax for strings, which is to have the value quoted.

fps
  • 33,623
  • 8
  • 55
  • 110
  • 2
    Ugh I just realized it was an instance method in my example this whole time. Sloppy. Thanks for pointing this out. – jwilner Jul 29 '15 at 01:26
  • I want Jackson to deserialize `READY`, `NOT_READY`, and `NOT_READY_AT_ALL` addditionally to `ready`, `notReady`, and `notReadyAtAll`. How can I do it? I may insert `try { return Status.valueOf(string); } catch (IllegalArgumentException e) {}` at the start of fromString(). Is there a better solution? – Johannes Flügel Apr 06 '17 at 12:46
  • @FedericoPeraltaSchaffner I want to deserialize both. – Johannes Flügel Apr 06 '17 at 13:16
  • 2
    @JohannesFlügel I'd just modify the factory method: if the string argument is not in the map, I'd try with `Status.valueOf`. In one line: `return Optional.ofNullable(FORMAT_MAP.get(string)).orElseGet(() -> Status.valueOf(string));` – fps Apr 06 '17 at 13:58
  • Except in the code above, you are trying to access an instance variable "formatted" from within a static context. It won't compile this way – Kevin M May 23 '17 at 20:24
  • How can you do the @JsonCreator method in Kotlin? – B W Jun 08 '17 at 19:58
  • 1
    Thank you, thank you, thank you! I actually had one library that was built with newer jackson and uses @ JsonProperty. But some of my dependencies were pulling older jackson (2.5.x). So code compiled just fine but @JsonProperty value wasn't honored and instead enum constant name was used ("AAA" instead of "aaa" specified in @ JsonProperty). – interrupt Jun 21 '17 at 05:29
  • @interrupt Glad you notice the issue. You were another victim of dependency hell... If you are using maven, you should fix the version in the dependency management section of your master pom. – fps Jun 21 '17 at 15:20
  • My input is a xml, how can i achieve it for xml ? – Mohamed Thoufeeque Jul 11 '18 at 13:06
  • @MohamedThoufeeque Hi! Sorry, I've never used Jackson for XML. – fps Jul 11 '18 at 20:39
  • 1
    this works only if you have only one field and if JsonFormat shape is not as object. this is for multiple fields and @JsonFormat(shape = JsonFormat.Shape.OBJECT) @JsonCreator static PinOperationMode findValue(@JsonProperty("mode") String mode, @JsonProperty("code") String code) { return Arrays.stream(PinOperationMode.values()).filter(pt -> pt.mode.equals(mode) && pt.code.equals(code)).findFirst().get(); } – Stefan Feb 06 '19 at 19:22
  • @Stefan Hi, I haven't tested it with other setups because the question was about only one field and said nothing about `@JsonFormat`. It's good to know that you made it work for more fields, though. Cheers! – fps Feb 06 '19 at 19:24
  • 1
    @FedericoPeraltaSchaffner no problem, i posted the answer for this case below. cheers – Stefan Feb 07 '19 at 19:14
  • is there any way, with jackson, to do this with a plain old string instead of a json string? i.e. if the input to say a restful endpoint is a request header with a string value of `ready` not `"ready"`. Is there any way for jackson to recognize `ready` as a string literal value to be passed to `fromString` instead of requiring it be a json string like `"ready"`? – Achilles929 Jul 14 '23 at 17:33
  • @Achilles929 I don't know if there's some config flag to render JSON strings as invalid json strings, i.e. without quotes. You can always do some hacking, though, but it would be out of the scope of this question/answer. See here: https://stackoverflow.com/questions/18164435/jackson-field-value-with-no-quotation-marks – fps Jul 14 '23 at 20:22
12

This is probably a faster way to do it:

public enum Status {
 READY("ready"),
 NOT_READY("notReady"),
 NOT_READY_AT_ALL("notReadyAtAll");

 private final String formatted;

 Status(String formatted) {
   this.formatted = formatted;
 }

 @Override
 public String toString() {
   return formatted;
 }
}

public static void main(String[] args) throws IOException {
  ObjectMapper mapper = new ObjectMapper();
  ObjectReader reader = mapper.reader(Status.class);
  Status status = reader.with(DeserializationFeature.READ_ENUMS_USING_TO_STRING).readValue("\"notReady\"");
  System.out.println(status.name());  // NOT_READY
}
9
@JsonCreator
public static Status forValue(String name)
{
    return EnumUtil.getEnumByNameIgnoreCase(Status.class, name);
}

Adding this static method would resolve your problem of deserializing

Arun Kumar
  • 91
  • 1
  • 2
6

For whoever is searching for enums with integer json properties. Here is what worked for me:

enum class Status (private val code: Int) {
    PAST(0),
    LIVE(2),
    UPCOMING(1);
    companion object {
        private val codes = Status.values().associateBy(Status::code)
        @JvmStatic @JsonCreator fun from (value: Int) = codes[value]
    }
}
Sileria
  • 15,223
  • 4
  • 49
  • 28
5

@JsonCreator(mode = JsonCreator.Mode.DELEGATING) was the solution for me.

https://github.com/FasterXML/jackson-module-kotlin/issues/336#issuecomment-630587525

4b0
  • 21,981
  • 30
  • 95
  • 142
Sen'IT
  • 66
  • 2
  • 4
4

The solutions on this page work only for single field and @JsonFormat(shape = JsonFormat.Shape.NATURAL) (default format)

this works for multiple fields and @JsonFormat(shape = JsonFormat.Shape.OBJECT)

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum PinOperationMode {
    INPUT("Input", "I"),
    OUTPUT("Output", "O")
    ;

    private final String mode;
    private final String code;

    PinOperationMode(String mode, String code) {
        this.mode = mode;
        this.code = code;
    }

    public String getMode() {
        return mode;
    }

    public String getCode() {
        return code;
    }

    @JsonCreator
    static PinOperationMode findValue(@JsonProperty("mode") String mode, @JsonProperty("code") String code) {
        return Arrays.stream(PinOperationMode.values()).filter(pt -> pt.mode.equals(mode) && pt.code.equals(code)).findFirst().get();
    }
}
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Stefan
  • 322
  • 2
  • 7
  • 19
1

You could use @JsonCreator annotation to resolve your problem. Take a look at https://www.baeldung.com/jackson-serialize-enums, there's clear enough explanation about enum and serialize-deserialize with jackson lib.

alhamwa
  • 79
  • 5