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

Closed
tgolden-andplus opened this issue Dec 5, 2017 · 25 comments
Closed
Labels
has-failing-test Indicates that there exists a test case (under `failing/`) to reproduce the issue most-wanted Tag to indicate that there is heavy user +1'ing action
Milestone

Comments

@tgolden-andplus
Copy link

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
Copy link
Member

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
Copy link
Author

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
Copy link
Member

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

@angrygoats
Copy link

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
Copy link
Member

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
Copy link

vghero commented Apr 24, 2018

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

@joaoe
Copy link

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 yes, then do NOT do enum value lookup by index, but instead lookup the enum value using the same rule as the String one, regardless if FAIL_ON_NUMBERS_FOR_ENUMS is set,

This is the testcase.
Enums using int as @JsonValue should parse correctly.
The test should run without asserts.

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;

        System.out.println("PASS");
    }
}

@cowtowncoder
Copy link
Member

@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
Copy link

joaoe commented Nov 17, 2018

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

@millevlada
Copy link

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
Copy link
Member

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
Copy link

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

@helio4987
Copy link

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

LukeChow,
Probably you already figured it out, but you just need to add @JvmStatic before the @JsonCreator on the method inside the companion object, and it should work.

@cowtowncoder cowtowncoder added 2.12 and removed 2.11 labels Apr 12, 2020
@cowtowncoder cowtowncoder added 2.13 has-failing-test Indicates that there exists a test case (under `failing/`) to reproduce the issue and removed 2.12 labels Oct 27, 2020
@fkabakci
Copy link

fkabakci commented Nov 7, 2020

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 ;)

this worked perfect in my case. thanks a lot @millevlada

@cowtowncoder cowtowncoder added the most-wanted Tag to indicate that there is heavy user +1'ing action label Feb 20, 2021
@cowtowncoder
Copy link
Member

Note: had a dup (#2754).

@srinivasreddyp
Copy link

srinivasreddyp commented May 7, 2021

@cowtowncoder

We still see the issue in deserialization of the enum of type Integer not with primitive type.

This is happening after Jackson library upgrade to 2.11.2 version of the library from 2.9.7, it also exists in the version 2.12.4

Our code was a generated one and generated fine with both required annotations properly.

The generated enum from the swagger yaml file is below:

public enum MyEnum {
    NUMBER_1(1),
    NUMBER_2(2);

    private Integer code;

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

   @JsonValue
   public Integer getValue() {
        return getCode();
   }

 @JsonCreator
   public static MyEnum fronValue(String code) {
       for (MyEnum element : MyEnum.values()) {
          if (String.valueOf(element.code).equals( code) {
             return element;
          }
      }
      return null;
   }

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

Pojo Class using the enum type:

public class Info {
@JsonProperty(“count”)
 MyEnum count;
}

Input Json to parse:

{ count: 1 } 

Exception occurred when tried to parse:
Below is a sample code from the JUnit unit test case which can throw error.

ObjectMapper mapper = new ObjectMapper();
mapper.readValue(“ { count: 1 } ”, Info.class);  ——-> Unable to parse to type Info.class

Throwing com.fasterxml.jackson.databind.exc.ValueInstantiationException.from(ValueInstantiationException.Java:47)

Can you or someone already has a solution to this issue?

Any help will be greatly appreciated.

@cowtowncoder
Copy link
Member

cowtowncoder commented May 8, 2021

@srinivasreddyp I am not sure I understand -- this issue is open so I don't see how it would have been solved.

Example has some problems too: content included is invalid JSON (missing double-quotes around count) but I assume that is not related.

Biggger problem, and something that SHOULD throw an exception (but does not seem to is that this is illegal:


 @JsonCreator
   public static MyEnum fronValue(String code) {
       for (MyEnum element : MyEnum.values()) {
          if (String.valueOf(element.code).equals( code) {
             return element;
          }
      }
      return null;
   }

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

as you cannot have 2 @JsonCreator annotated factory methods with same input type.
Maybe that's a typo or something since former probably should take int or Integer -- but there cannot be 2 conflicting creators.

I don't know if your case is possible to work around right now.

One solution that would work would be to have @JsonCreator annotated factory method that take java.lang.Object (or JsonNode): that would be called in both JSON String and Number case.

@srinivasreddyp
Copy link

srinivasreddyp commented May 8, 2021

Apologies for the typos, I was not supposed to paste in the actual code so I tried to type in manually which lead to typos in code above.

Here is the live example which will produce the exception with the Jackson library versions 2.11.2 & 2.12.3

The same is running fine with the Jackson 2.9.7 version.

The issue is only with the Deserialization, where as the Serialization is good.

FileName: Info.java

`package com.jackson.test;

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

public class Info {

public static void main(String[] args) throws Exception {
String requestJson = "{ "count": 1 }";
ObjectMapper mapper = new ObjectMapper();
Data data = mapper.readValue(requestJson, Data.class);
System.out.println("Count: " + data.count);
System.out.println(mapper.writeValueAsString(data));
}
}

class Data {
@JsonProperty
MyEnum count;
}

enum MyEnum {
NUMBER_1(1),
NUMBER_2(2);

private Integer code;

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

@JsonCreator
public static MyEnum fromValue(String code) {
for (MyEnum element : MyEnum.values()) {
if (String.valueOf(element.code).equals(code)) {
return element;
}
}
return null;
}

@JsonValue
public Integer getValue() {
return code;
}

@OverRide
public String toString() {
return String.valueOf(code);
}
}`

Below is the exception stack trace after running the above code:

Exception in thread "main" com.fasterxml.jackson.databind.exc.ValueInstantiationException: Cannot construct instance of com.jackson.test.MyEnum, problem: argument type mismatch
at [Source: (String)"{ "count": 1 }"; line: 1, column: 12] (through reference chain: com.jackson.test.Data["count"])
at com.fasterxml.jackson.databind.exc.ValueInstantiationException.from(ValueInstantiationException.java:47)
at com.fasterxml.jackson.databind.DeserializationContext.instantiationException(DeserializationContext.java:1907)
at com.fasterxml.jackson.databind.DeserializationContext.handleInstantiationProblem(DeserializationContext.java:1260)
at com.fasterxml.jackson.databind.deser.std.FactoryBasedEnumDeserializer.deserialize(FactoryBasedEnumDeserializer.java:168)
at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:138)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:324)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:187)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3548)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3516)
at com.jackson.test.Info.main(Info.java:14)
Caused by: java.lang.IllegalArgumentException: argument type mismatch
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.fasterxml.jackson.databind.introspect.AnnotatedMethod.callOnWith(AnnotatedMethod.java:115)
at com.fasterxml.jackson.databind.deser.std.FactoryBasedEnumDeserializer.deserialize(FactoryBasedEnumDeserializer.java:160)
... 8 more

The same code is working fine if the json received to parse is holding the value of type String instead of Integer.

FileName: Info.java

`package com.jackson.test;

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

public class Info {

public static void main(String[] args) throws Exception {
String requestJson = "{ "count": "1" }";
ObjectMapper mapper = new ObjectMapper();
Data data = mapper.readValue(requestJson, Data.class);
System.out.println("Count: " + data.count);
System.out.println(mapper.writeValueAsString(data));
}
}

class Data {
@JsonProperty
MyEnum count;
}

enum MyEnum {
NUMBER_1(1),
NUMBER_2(2);

private Integer code;

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

@JsonCreator
public static MyEnum fromValue(String code) {
for (MyEnum element : MyEnum.values()) {
if (String.valueOf(element.code).equals(code)) {
return element;
}
}
return null;
}

@JsonValue
public Integer getValue() {
return code;
}

@OverRide
public String toString() {
return String.valueOf(code);
}
}`

Output after running the above code:

Count: 1

Process finished with exit code 0

Please run the above code with the versions I specified, hope this will help understand my issue and reproduce the problem.

@cowtowncoder
Copy link
Member

I don't think we are short of examples per se. I just don't really have time to work on this issue, unfortunately.
So I trust that handling is suboptimal and that to a degree part(s) of the problem may be regression.
I am not looking for confirmation that things are not working as well as they should; although I try to point out things that would not be allowed (like ambiguous methods for @JsonCreator) to help isolate some issues.

A big challenge at code level is that handling of Enum types is quite different from that of POJOs. Part of this is due to natural differences: Enums are quite different things from Bean-style POJOs (enum values are canonical).
But there are some similarities too, and in particular it would be great if code for handling @JsonCreator could be reused across these types -- alas, it is not, and cannot easily.

@srinivasreddyp
Copy link

np .. thank you !

@srinivasreddyp
Copy link

@cowtowncoder

I have investigated this issue further, the issue for our case lies in the swagger code generator rather than Jackson

https://github.com/srinivasreddyp/swagger-codegen-generators/blob/master/src/main/resources/handlebars/JavaInflector/enumClass.mustache

The link above has a template code which is used to generate the Pojos out of swagger yaml.

Probably the fix is required in their library to consider the Object type as a parameter over the method annotated with @JsonCreator

eg: Below code is working for any Java type.

@JsonCreator public static MyEnum fromValue(Object code) { for (MyEnum element : MyEnum.values()) { if (element.code.equals(code)) { return element; } } return null; }

Will try to push a patch to swagger code generator plugin.
Thank you !

@cowtowncoder
Copy link
Member

@srinivasreddyp Ok I am glad you found out possible way forward. Handling of Enum creators is bit tricky but generator should be able to produce a working combination.

tailoric added a commit to tailoric/spring-boot-discord-slashcommand-test that referenced this issue May 30, 2021
Jackson always uses the ordinal number instead of the integer
this fix was posted on the comments of the issue regarding this problem:

FasterXML/jackson-databind#1850 (comment)
@spectral-butters
Copy link

I've managed to fix it by setting @JsonCreator mode to DELEGATING and having @JsonValue on my id field
e.g.

SOME_VALUE(1),
OTHER_VALUE(2)

@JsonValue
private Integer id;

// getter   

@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
static MyEnum fromId(int id){
    return Stream.of(MyEnum.values()).filter(enum -> enum.id == id).findFirst().get();
}

@cowtowncoder cowtowncoder added 2.14 and removed 2.13 labels Jul 15, 2021
@pawan-terdal
Copy link

@cowtowncoder

I have investigated this issue further, the issue for our case lies in the swagger code generator rather than Jackson

https://github.com/srinivasreddyp/swagger-codegen-generators/blob/master/src/main/resources/handlebars/JavaInflector/enumClass.mustache

The link above has a template code which is used to generate the Pojos out of swagger yaml.

Probably the fix is required in their library to consider the Object type as a parameter over the method annotated with @JsonCreator

eg: Below code is working for any Java type.

@JsonCreator public static MyEnum fromValue(Object code) { for (MyEnum element : MyEnum.values()) { if (element.code.equals(code)) { return element; } } return null; }

Will try to push a patch to swagger code generator plugin.
Thank you !

Is the fix released ? If yes, in which version ?

@cowtowncoder
Copy link
Member

Thanks to @limengning's PR, will shortly fix this!

@cowtowncoder cowtowncoder changed the title @JsonValue with integer for enum does not deserialize correctly @JsonValue with integer for enum does not deserialize correctly Sep 27, 2021
cowtowncoder added a commit that referenced this issue Sep 27, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
has-failing-test Indicates that there exists a test case (under `failing/`) to reproduce the issue most-wanted Tag to indicate that there is heavy user +1'ing action
Projects
None yet
Development

No branches or pull requests