Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@JsonValue with integer for enum does not deserialize correctly #1850

Open
tgolden-andplus opened this issue Dec 5, 2017 · 12 comments
Open

@JsonValue with integer for enum does not deserialize correctly #1850

tgolden-andplus opened this issue Dec 5, 2017 · 12 comments
Labels

Comments

@tgolden-andplus
Copy link

@tgolden-andplus tgolden-andplus commented Dec 5, 2017

The Javadoc for @JsonValue states that it is the only required annotation to both serialize and deserialize enums as something other than their name or ordinal:

when use for Java enums, one additional feature is that value returned by annotated method is also considered to be the value to deserialize from, not just JSON String to serialize as. This is possible since set of Enum values is constant and it is possible to define mapping, but can not be done in general for POJO types; as such, this is not used for POJO deserialization.

The Javadoc also states that it can be applied to any scalar type:

Usually value will be of a simple scalar type (String or Number), but it can be any serializable type (Collection, Map or Bean).

However, annotating an enum like below will fail to deserialize -- Jackson appears to interpret the integer as the ordinal of the enum.

public enum Type {
    A(2),
    B(3);
    private final int value;
    Type(final int value) { this.value = value; }
    @JsonValue public int value() { return this.value; }
}

When attempting to deserialize an enum like the above example, on Jackson 2.9.2, I receive the following stack trace: (slightly anonymized)

com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `com.example.Type` from number 3: index value outside legal index range [0..1]
                                                                                 at [Source: (InputStreamReader); line: 1, column: 60] (through reference chain: java.util.ArrayList[0]->com.example.Pojo["type"])
                                                                                 at com.fasterxml.jackson.databind.DeserializationContext.weirdNumberException(DeserializationContext.java:1563)
                                                                                 at com.fasterxml.jackson.databind.DeserializationContext.handleWeirdNumberValue(DeserializationContext.java:953)
                                                                                 at com.fasterxml.jackson.databind.deser.std.EnumDeserializer.deserialize(EnumDeserializer.java:200)
                                                                                 at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeSetAndReturn(MethodProperty.java:149)

If I add a @JsonCreator to a static method that matches the value to the internal field, the enum can be deserialized correctly. However that is more code I would rather not maintain if possible.

@cowtowncoder

This comment has been minimized.

Copy link
Member

@cowtowncoder cowtowncoder commented Dec 5, 2017

Ok. Yes, it seems that implict rule of using index overrides delegating construction for integers in this case. I am not 100% if this should or should not work, however, since there is actually one more step that in general is needed: addition of Creator method annotated with @JsonCreator.

This is not needed for Strings, but would be needed for most other types. But I can see why it would make sense to also not be needed in case of int or long.

I think I'll need to investigate if and how this could be made to work; it seems doable but internal handling does get bit complicated as this is a special case.

@tgolden-andplus

This comment has been minimized.

Copy link
Author

@tgolden-andplus tgolden-andplus commented Dec 5, 2017

Yes, I can see how it's not a common case. If it's deemed not worthy of inclusion, it might help to clarify the javadocs on @JsonValue that the auto-magic deserialization does not work for ints and the client should provide @JsonCreator as they would normally.

@cowtowncoder

This comment has been minimized.

Copy link
Member

@cowtowncoder cowtowncoder commented Dec 6, 2017

@tgolden-andplus Yes that would definitely make sense at very least.

@angrygoats

This comment has been minimized.

Copy link

@angrygoats angrygoats commented Mar 29, 2018

I have also recently experienced this and it was certainly a mystery trying to figure it out. In my case it didn't fail, it just picked the wrong enum (the corresponding ordinal in the enum list) rather than the enum I expected. Very, very confusing.

@cowtowncoder

This comment has been minimized.

Copy link
Member

@cowtowncoder cowtowncoder commented Mar 29, 2018

One more thing: it is not that @JsonValue does not work with ints, it does. But that handling of @JsonValue with enums is problematic. Or most accurately, combination of enum type and int is ambiguous as things are.

@vghero

This comment has been minimized.

Copy link

@vghero vghero commented Apr 24, 2018

I also stumbled across this today. Would be cool if that could be handled transparently for enums :).

@joaoe

This comment has been minimized.

Copy link

@joaoe joaoe commented Nov 16, 2018

Howdy. I just hit this problem.

I actually had set FAIL_ON_NUMBERS_FOR_ENUMS to true to skip evaluating ints as the index of the enum value. But the code crashes anyway.

I see that the problem lies in EnumDeserializer.deserialize(). For Strings, it looks up the enum value, as given by @JsonValue. For ints, it looks up the enum value by position.

My suggestion is the following: keep track of if @JsonValue is an int. If not, use the current behavior. If so, then do NOT do enum value lookup by index, but instead lookup the enum value using the same rule as the String one.
Also, in the latter case, if FAIL_ON_NUMBERS_FOR_ENUMS is set, My enum that uses int as @JsonValue should parse correctly.

This is my testcase btw

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

import java.io.IOException;

public class MyExperiments {
    enum Example1 {
        A(101);
        final int x;
        Example1(int x) { this.x = x; }
        @JsonValue
        public int code() { return x; }
    }

    enum Example2 {
        A(202);
        @JsonValue
        public final int x;
        Example2(int x) { this.x = x; }
    }

    public static void main(String[] args) throws IOException {
        ObjectMapper m = new ObjectMapper();
        m.configure(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, true);

        String s1 = m.writer().writeValueAsString(Example1.A);
        String s2 = m.writer().writeValueAsString(Example2.A);

        System.out.println("1: " + s1);
        System.out.println("2: " + s2);

        Example1 e1 = m.readValue(s1, Example1.class);
        Example2 e2 = m.readValue(s2, Example2.class);

        System.out.println("1: " + e1);
        System.out.println("2: " + e2);

        assert e1 == Example1.A : e1;
        assert e2 == Example2.A : e2;
    }
}
@cowtowncoder

This comment has been minimized.

Copy link
Member

@cowtowncoder cowtowncoder commented Nov 17, 2018

@joaoe Could you please create a new issue and your test case there? While issues look similar they may not be the same and it is easier to track them as separate entries.

@joaoe

This comment has been minimized.

Copy link

@joaoe joaoe commented Nov 17, 2018

Hi. My issue is exactly the same as the one described originally.

@millevlada

This comment has been minimized.

Copy link

@millevlada millevlada commented Dec 7, 2018

Hi, I had same issue today, I managed to resolve that using this 2 annotations implemented within my
int coded enum:

public enum MyEnum {
    SOME_MEMBER1(10),
    SOME_MEMBER2(15),

    private int code;

    MyEnum(int code) {
       this.code = code;
    }

   public int getCode() {
      return code;
   }

   @JsonValue
   public int toValue() {
        return getCode();
   }

   public static MyEnum forCode(int code) {
       for (MyEnum element : values()) {
          if (element.code == code) {
             return element;
          }
      }
      return null; //or throw exception if you like...
   }

   @JsonCreator
   public static MyEnum forValue(String v) {
       return MyEnum.forCode(Integer.parseInt(v));
   } 
}

Hope that helps someone else too, I did spent quite some time to find the variant that works...
Cheers ;)

@cowtowncoder cowtowncoder added the 2.11 label Nov 28, 2019
@cowtowncoder

This comment has been minimized.

Copy link
Member

@cowtowncoder cowtowncoder commented Dec 7, 2019

Ok so yes, while @JsonValue is used both for serialization (value written as-is) and deserialization, in latter case it is current (up to 2.10) unfortunately just coerced into a String.
Specific method where this is handled is EnumResolver.constructUsingMethod().
What is needed is probably extension of EnumResolver but it is not trivial change, even if just limiting to int/long (and wrappers).

I'll add a failing test and hopefully someone at some point has time to improve this; change could go in 2.11 (since I am pretty sure internal API at least has to change to accommodate need to pass typed lookup info).

cowtowncoder added a commit that referenced this issue Dec 7, 2019
@LukeChow

This comment has been minimized.

Copy link

@LukeChow LukeChow commented Dec 27, 2019

Hi, I had same issue today, I managed to resolve that using this 2 annotations implemented within my
int coded enum:

public enum MyEnum {
    SOME_MEMBER1(10),
    SOME_MEMBER2(15),

    private int code;

    MyEnum(int code) {
       this.code = code;
    }

   public int getCode() {
      return code;
   }

   @JsonValue
   public int toValue() {
        return getCode();
   }

   public static MyEnum forCode(int code) {
       for (MyEnum element : values()) {
          if (element.code == code) {
             return element;
          }
      }
      return null; //or throw exception if you like...
   }

   @JsonCreator
   public static MyEnum forValue(String v) {
       return MyEnum.forCode(Integer.parseInt(v));
   } 
}

Hope that helps someone else too, I did spent quite some time to find the variant that works...
Cheers ;)

How to do it in Kotlin? There is no static method in Kotlin but companion object method, which not works by using annotation @JsonCreator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
7 participants
You can’t perform that action at this time.