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.
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.
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.
@tgolden-andplus Yes that would definitely make sense at very least.
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.
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.
I also stumbled across this today. Would be cool if that could be handled transparently for enums :).
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.
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");
}
}
@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.
Hi. My issue is exactly the same as the one described originally.
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 ;)
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).
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
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.
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
Note: had a dup (#2754).
@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.
@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.
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.
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.
np .. thank you !
@cowtowncoder
I have investigated this issue further, the issue for our case lies in the swagger code generator rather than Jackson
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 !
@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.
Most helpful comment
Hi, I had same issue today, I managed to resolve that using this 2 annotations implemented within my
int coded enum:
Hope that helps someone else too, I did spent quite some time to find the variant that works...
Cheers ;)