Search before asking
- [x] I searched in the issues and found nothing similar.
Describe the bug
EnumDeserializer fails to deserialize Enums with @JsonValue - uses table with name() key instead of @JsonValue key.
The difference between Jackson2 and Jackson3 is the choice of tables used for lookup (see debugger screen-shot).
@Test
void convertStringToEnum() {
assertThat(new ObjectMapper().convertValue("10%", MyEnum.class)).isEqualTo(MyEnum.T10);
}
public enum MyEnum {
T10("10%"), T20("20%"), T30("30%");
private final String code;
MyEnum(String code) {
this.code = code;
}
@JsonValue
public String getCode() {
return code;
}
}
tools.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `org.springframework.data.couchbase.core.mapping.MappingCouchbaseConverterTests$MyEnum` from String "10%": not one of the values accepted for Enum class: [T30, T20, T10]
at [No location information]
at tools.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:37)
at tools.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1977)
at tools.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:1320)
at tools.jackson.databind.deser.jdk.EnumDeserializer._deserializeAltString(EnumDeserializer.java:355)
at tools.jackson.databind.deser.jdk.EnumDeserializer._fromString(EnumDeserializer.java:215)
at tools.jackson.databind.deser.jdk.EnumDeserializer.deserialize(EnumDeserializer.java:184)
at tools.jackson.databind.ObjectMapper._convert(ObjectMapper.java:2391)
at tools.jackson.databind.ObjectMapper.convertValue(ObjectMapper.java:2323)
In the debugger
With Jackson3, the lookup table has the name() as the key. Thus it does not find the corresponding Enum.
With Jackson2 the lookup table has the @JsonValue values as the key. It does find the corresponding Enum using the serialized value.
Expected behavior
Test passes.
Additional context
Works in Jackson 2
Comment From: JooHyukKim
Thank you for reporting! Seems like a regression indeed
Comment From: mikereiche
In Jackson3 the (ctx.feature_flags & READ_ENUMS_USING_TO_STRING != 0) == true
00011100110000011110010010100000
00000000100000000000000000000000
in Jackson2, (ctx.feature_flags & READ_ENUMS_USING_TO_STRING !=0 ) == false
01110001000001010010010010010000
00000010000000000000000000000000
private CompactStringObjectMap _resolveCurrentLookup(DeserializationContext ctxt) {
if (_lookupByEnumNaming != null) {
return _lookupByEnumNaming;
}
return ctxt.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
? _lookupByToString
: _lookupByName;
}
Comment From: mikereiche
/**
* Feature that determines the deserialization mechanism used for
* Enum values: if enabled, Enums are assumed to have been serialized using
* return value of {@code Enum.toString()};
* if disabled, return value of {@code Enum.name()} is assumed to have been used.
*<p>
* Note: this feature should usually have same value
* as {@link SerializationFeature#WRITE_ENUMS_USING_TO_STRING}.
*<p>
* Feature is enabled by default as of Jackson 3.0 (in 2.x it was disabled). <--------------------------------------------
*/
READ_ENUMS_USING_TO_STRING(true),
Comment From: mikereiche
This will fix the enum. But shouldn't it work without it?
@Override
String toString(){
return jsonValue():
}
Comment From: pjfanning
In Jackson 3, this has moved to EnumFeature and default has changed.
/**
* Feature that determines the deserialization mechanism used for
* Enum values: if enabled, Enums are assumed to have been serialized using
* return value of {@code Enum.toString()};
* if disabled, return value of {@code Enum.name()} is assumed to have been used.
*<p>
* Note: this feature should usually have same value
* as {@link #WRITE_ENUMS_USING_TO_STRING}.
*<p>
* Feature used to be one of {@link tools.jackson.databind.DeserializationFeature}s
* in Jackson 2.x but was moved here in 3.0.
*<p>
* Feature is enabled by default as of Jackson 3.0 (in 2.x it was disabled).
*/
READ_ENUMS_USING_TO_STRING(true),
Comment From: mikereiche
When an enum has an @JsonValue annotated method, the result of that method is used for serialization, thus the result of that method for each of the enums needs to be the keys for the lookup table for deserialization.
Comment From: JooHyukKim
Like PJ said above, there are WRITE_ENUMS_XXX and READ_ENUMS_XXX features to support separate cases for serialization/deserialization.
the keys for the lookup table for deserialization.
It seems that what you refering to is what default values of READ_ENUMS_XXX features should be? or no? @mikereiche may I ask if you are referring to something else?
Comment From: mikereiche
For enums that have an @JsonValue annotated method, - since the default is now to assume that toString() was used to serialize the enum, then making toString() output the same value as the @JsonValue method will make it work. I don't know how to explain it any more plainly than that. I suggest you try it and prove it to yourself.
However - there is no need to assume that toString() or anything else is used to serialize enums that have a method annotated with @JsonVakue. @JsonValue is a Jackson annotation that indicates that the ObjectMapper (also a Jackson class) is to use that method to serialize the Object to json.
Comment From: JooHyukKim
@mikereiche OK thank you for explanation! I I wanted to check if I understood correctly👍👍
I will try to look into this maybe tonight ?
Comment From: cowtowncoder
I agree with @mikereiche : use of @JsonValue should have precedence over other options. So regardless of EnumFeature defaults or settings, return value of @JsonValue annotated accessor should be used for serialization AND deserialization.
Comment From: JooHyukKim
While digging, I found more information.
use of @JsonValue should have precedence over other options. So regardless of EnumFeature defaults or settings, return value of @JsonValue annotated accessor should be used for serialization AND deserialization.
Above statement may have been broken long time ago. I wrote a reproducer against 2.16 https://github.com/FasterXML/jackson-databind/pull/5276 throwing same error which you may find in this Action.
And yes, you guys were right, @JsonValue is described to provide symmetric deser/serialization behavior.
Comment From: JooHyukKim
Just tested against 2.12.5 also. Expected behavior seems to have only worked because DeserializationFeature.READ_ENUMS_USING_TO_STRING was disabled by default.
We have already lost @JsonValue context by the time we reach EnumDeserializer._resolveCurrentLookup(DeserializationContext).
Idea
- Add a boolean field useJsonValue(draft) in EnumDeserializer to be calculated during construction.
- I think we can get this information vai constructUsingMethod().
- Use this new flag during _resolveCurrentLookup to prioritize before EnumFeatures
Comment From: cowtowncoder
Ok yes, unfortunately EnumDeserializer is bit of a mess. But makes sense problem was masked.
Would be great to fix for 2.20 if at all possible.
Comment From: cowtowncoder
@JooHyukKim something like that should work. Basically prevent use of "toString()" lookup index.