- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 1.4k
 
          [Jackson 3] Fix #3580 Enum (de)serialization in conjunction with JsonFormat.Shape.NUMBER_INT
          #5338
        
          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
Changes from 8 commits
7bb4802
              be78ac9
              27cea5d
              783f39c
              f9ba89b
              91bff73
              ba2dfa7
              41bec28
              9e21605
              4850211
              2735c53
              1706b03
              8364974
              145b059
              5fca841
              55b2902
              c6ddb8d
              0318b9f
              8c99597
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| 
          
            
          
           | 
    @@ -65,8 +65,20 @@ public class EnumDeserializer | |
| */ | ||
| protected final CompactStringObjectMap _lookupByEnumNaming; | ||
| 
     | 
||
| /** | ||
| * We may also have integer-type of representation for Enum's, along with `@JsonValue`. | ||
| * | ||
                
      
                  JooHyukKim marked this conversation as resolved.
               
              
                Outdated
          
            Show resolved
            Hide resolved
         | 
||
| */ | ||
| protected final CompactStringObjectMap _lookupByShapeNumberInt; | ||
| 
     | 
||
| /** | ||
| * Flag to check if FormatShape of int number type would be used to deserialize | ||
| * | ||
                
      
                  JooHyukKim marked this conversation as resolved.
               
              
                Outdated
          
            Show resolved
            Hide resolved
         | 
||
| */ | ||
| protected final boolean _isShapeNumberInt; | ||
| 
     | 
||
| public EnumDeserializer(EnumResolver byNameResolver, boolean caseInsensitive, | ||
                
       | 
||
| EnumResolver byEnumNamingResolver, EnumResolver toStringResolver) | ||
| EnumResolver byEnumNamingResolver, EnumResolver toStringResolver, EnumResolver shapeNumberResolver) | ||
| { | ||
| super(byNameResolver.getEnumClass()); | ||
| _lookupByName = byNameResolver.constructLookup(); | ||
| 
        
          
        
         | 
    @@ -75,8 +87,10 @@ public EnumDeserializer(EnumResolver byNameResolver, boolean caseInsensitive, | |
| _enumDefaultValue = byNameResolver.getDefaultValue(); | ||
| _caseInsensitive = caseInsensitive; | ||
| _isFromIntValue = byNameResolver.isFromIntValue(); | ||
| _isShapeNumberInt = shapeNumberResolver != null; | ||
| _lookupByEnumNaming = byEnumNamingResolver == null ? null : byEnumNamingResolver.constructLookup(); | ||
| _lookupByToString = toStringResolver == null ? null : toStringResolver.constructLookup(); | ||
| _lookupByShapeNumberInt = shapeNumberResolver == null ? null : shapeNumberResolver.constructLookup(); | ||
| } | ||
| 
     | 
||
| protected EnumDeserializer(EnumDeserializer base, boolean caseInsensitive, | ||
| 
        
          
        
         | 
    @@ -89,10 +103,13 @@ protected EnumDeserializer(EnumDeserializer base, boolean caseInsensitive, | |
| _enumDefaultValue = base._enumDefaultValue; | ||
| _caseInsensitive = caseInsensitive; | ||
| _isFromIntValue = base._isFromIntValue; | ||
| _isShapeNumberInt = base._isShapeNumberInt; | ||
| _useDefaultValueForUnknownEnum = useDefaultValueForUnknownEnum; | ||
| _useNullForUnknownEnum = useNullForUnknownEnum; | ||
| _lookupByEnumNaming = base._lookupByEnumNaming; | ||
| _lookupByToString = base._lookupByToString; | ||
| _lookupByShapeNumberInt = base._lookupByShapeNumberInt; | ||
| 
     | 
||
| } | ||
| 
     | 
||
| /** | ||
| 
          
            
          
           | 
    @@ -194,6 +211,10 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) | |
| // 26-Sep-2021, tatu: [databind#1850] Special case where we get "true" integer | ||
| // enumeration and should avoid use of {@code Enum.index()} | ||
| if (_isFromIntValue) { | ||
| // [databind#3580] Enum (de)serialization in conjunction with JsonFormat.Shape.NUMBER_INT | ||
| if (_isShapeNumberInt) { | ||
| return _fromInteger(p, ctxt, p.getIntValue()); | ||
| } | ||
| // ... whether to rely on "getText()" returning String, or get number, convert? | ||
| // For now assume all format backends can produce String: | ||
| return _fromString(p, ctxt, p.getString()); | ||
| 
          
            
          
           | 
    @@ -238,7 +259,7 @@ private CompactStringObjectMap _resolveCurrentLookup(DeserializationContext ctxt | |
| } | ||
| 
     | 
||
| protected Object _fromInteger(JsonParser p, DeserializationContext ctxt, | ||
| int index) | ||
| int intValue) | ||
| throws JacksonException | ||
| { | ||
| final CoercionAction act = ctxt.findCoercionAction(logicalType(), handledType(), | ||
| 
        
          
        
         | 
    @@ -247,13 +268,13 @@ protected Object _fromInteger(JsonParser p, DeserializationContext ctxt, | |
| // First, check legacy setting for slightly different message | ||
| if (act == CoercionAction.Fail) { | ||
| if (ctxt.isEnabled(EnumFeature.FAIL_ON_NUMBERS_FOR_ENUMS)) { | ||
| return ctxt.handleWeirdNumberValue(_enumClass(), index, | ||
| return ctxt.handleWeirdNumberValue(_enumClass(), intValue, | ||
| "not allowed to deserialize Enum value out of number: disable DeserializationConfig.EnumFeature.FAIL_ON_NUMBERS_FOR_ENUMS to allow" | ||
| ); | ||
| } | ||
| // otherwise this will force failure with new setting | ||
| _checkCoercionFail(ctxt, act, handledType(), index, | ||
| "Integer value ("+index+")"); | ||
| _checkCoercionFail(ctxt, act, handledType(), intValue, | ||
| "Integer value ("+intValue+")"); | ||
| } | ||
| switch (act) { | ||
| case AsNull: | ||
| 
        
          
        
         | 
    @@ -263,14 +284,26 @@ protected Object _fromInteger(JsonParser p, DeserializationContext ctxt, | |
| case TryConvert: | ||
| default: | ||
| } | ||
| if (index >= 0 && index < _enumsByIndex.length) { | ||
| return _enumsByIndex[index]; | ||
| 
     | 
||
| // [databind#3580] Enum (de)serialization in conjunction with JsonFormat.Shape.NUMBER_INT | ||
| if (_isShapeNumberInt) { | ||
| Object numberShape = _lookupByShapeNumberInt.find(String.valueOf(intValue)); | ||
| if (numberShape != null) { | ||
| return numberShape; | ||
| } else { | ||
| return ctxt.handleWeirdNumberValue(_enumClass(), intValue, | ||
| "Number Int value is not one of expected values", | ||
| _lookupByShapeNumberInt.toString()); | ||
| } | ||
| } | ||
| if (intValue >= 0 && intValue < _enumsByIndex.length) { | ||
| return _enumsByIndex[intValue]; | ||
| } | ||
| if (useDefaultValueForUnknownEnum(ctxt)) { | ||
| return _enumDefaultValue; | ||
| } | ||
| if (!useNullForUnknownEnum(ctxt)) { | ||
| return ctxt.handleWeirdNumberValue(_enumClass(), index, | ||
| return ctxt.handleWeirdNumberValue(_enumClass(), intValue, | ||
| "index value outside legal index range [0..%s]", | ||
| _enumsByIndex.length-1); | ||
| } | ||
| 
          
            
          
           | 
    ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| 
          
            
          
           | 
    @@ -2,6 +2,7 @@ | |
| 
     | 
||
| import java.util.*; | ||
| 
     | 
||
| import com.fasterxml.jackson.annotation.JsonFormat; | ||
| import tools.jackson.databind.*; | ||
| import tools.jackson.databind.cfg.MapperConfig; | ||
| import tools.jackson.databind.introspect.AnnotatedClass; | ||
| 
          
            
          
           | 
    @@ -271,6 +272,52 @@ public static EnumResolver constructUsingMethod(DeserializationConfig config, | |
| ); | ||
| } | ||
| 
     | 
||
| /** | ||
| * Method used when ALL of conditions below are met | ||
| *<p> | ||
| * 1. actual String serialization is indicated using @JsonValue on a method in Enum class AND | ||
| * 2. Enum class is annotated with `@JsonFormat` | ||
                
      
                  JooHyukKim marked this conversation as resolved.
               
              
                Outdated
          
            Show resolved
            Hide resolved
         | 
||
| * | ||
| */ | ||
| public static EnumResolver constructUsingNumberShape(DeserializationConfig config, AnnotatedClass annotatedClass, AnnotatedMember accessor) | ||
                
      
                  JooHyukKim marked this conversation as resolved.
               
              
                Outdated
          
            Show resolved
            Hide resolved
         | 
||
| { | ||
| // prepare data | ||
| final boolean isIgnoreCase = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS); | ||
| final Class<?> enumCls0 = annotatedClass.getRawType(); | ||
| final Class<Enum<?>> enumCls = _enumClass(enumCls0); | ||
| final Enum<?>[] enumConstants = _enumConstants(enumCls); | ||
| final Enum<?> defaultEnum = _enumDefault(config, annotatedClass, enumConstants); | ||
| 
     | 
||
| // introspect | ||
| HashMap<String, Enum<?>> map = new HashMap<>(); | ||
| final AnnotationIntrospector ai = config.getAnnotationIntrospector(); | ||
| JsonFormat.Value format = ai.findFormat(config, annotatedClass); | ||
| if (format == null) { | ||
| return null; | ||
| } | ||
| if (format.getShape() != JsonFormat.Shape.NUMBER_INT) { | ||
| throw new IllegalArgumentException("Failed to access @JsonValue of Enum value "); | ||
                
      
                  JooHyukKim marked this conversation as resolved.
               
              
                Outdated
          
            Show resolved
            Hide resolved
         | 
||
| } | ||
| for (int i = enumConstants.length; --i >= 0; ) { // from last to first, so that in case of duplicate values, first wins | ||
| Enum<?> en = enumConstants[i]; | ||
| try { | ||
| Object o = accessor.getValue(en); | ||
                
       | 
||
| if (o != null) { | ||
| map.put(o.toString(), en); | ||
| } | ||
| } catch (Exception e) { | ||
| throw new IllegalArgumentException("Failed to access @JsonValue of Enum value "+en+": "+e.getMessage()); | ||
| } | ||
| } | ||
| 
     | 
||
| // finally build | ||
| return new EnumResolver(enumCls, enumConstants, map, defaultEnum, isIgnoreCase, | ||
| // 26-Sep-2021, tatu: [databind#1850] Need to consider "from int" case | ||
| _isIntType(accessor.getRawType()), | ||
| true | ||
| ); | ||
| } | ||
| 
     | 
||
| @SuppressWarnings("unchecked") | ||
| protected static Class<Enum<?>> _enumClass(Class<?> cls) { | ||
| return (Class<Enum<?>>) cls; | ||
| 
          
            
          
           | 
    ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package tools.jackson.databind.format; | ||
| 
     | 
||
| import com.fasterxml.jackson.annotation.JsonFormat; | ||
| import com.fasterxml.jackson.annotation.JsonValue; | ||
| import tools.jackson.databind.ObjectMapper; | ||
| import tools.jackson.databind.exc.InvalidFormatException; | ||
| import tools.jackson.databind.json.JsonMapper; | ||
| import tools.jackson.databind.testutil.DatabindTestUtil; | ||
| 
     | 
||
| import org.junit.jupiter.api.Test; | ||
| 
     | 
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||
| 
     | 
||
| // [databind#3580] Enum (de)serialization in conjunction with JsonFormat.Shape.NUMBER_INT | ||
| public class EnumNumberFormatShape3580Test | ||
| extends DatabindTestUtil | ||
| { | ||
| public static class Pojo3580 { | ||
| public PojoState3580 state; | ||
| public Pojo3580() {} | ||
| public Pojo3580(PojoState3580 state) {this.state = state;} | ||
| } | ||
| 
     | 
||
| @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) | ||
| public enum PojoState3580 { | ||
| OFF(17), | ||
| ON(31), | ||
| UNKNOWN(99); | ||
| 
     | 
||
| private int value; | ||
| 
     | 
||
| PojoState3580(int value) { this.value = value; } | ||
| 
     | 
||
| @JsonValue | ||
| public int value() {return this.value;} | ||
| } | ||
| 
     | 
||
| @Test | ||
| public void testEnumNumberFormatShape3580() | ||
| throws Exception | ||
| { | ||
| ObjectMapper mapper = JsonMapper.builder().build(); | ||
| 
     | 
||
| // Serialize | ||
| assertEquals("{\"state\":17}", mapper.writeValueAsString(new Pojo3580(PojoState3580.OFF))); // | ||
| assertEquals("{\"state\":31}", mapper.writeValueAsString(new Pojo3580(PojoState3580.ON))); // | ||
| assertEquals("{\"state\":99}", mapper.writeValueAsString(new Pojo3580(PojoState3580.UNKNOWN))); // | ||
| 
     | 
||
| // Pass Deserialize | ||
| assertEquals(PojoState3580.OFF, mapper.readValue("{\"state\":17}", Pojo3580.class).state); // Pojo[state=OFF] | ||
| assertEquals(PojoState3580.ON, mapper.readValue("{\"state\":31}", Pojo3580.class).state); // Pojo[state=OFF] | ||
| assertEquals(PojoState3580.UNKNOWN, mapper.readValue("{\"state\":99}", Pojo3580.class).state); // Pojo[state=OFF] | ||
| 
     | 
||
| // Fail : Try to use ordinal number | ||
| assertThrows(InvalidFormatException.class, () -> mapper.readValue("{\"state\":0}", Pojo3580.class)); | ||
| } | ||
| } | 
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package tools.jackson.databind.records; | ||
| 
     | 
||
| import com.fasterxml.jackson.annotation.JsonFormat; | ||
| import com.fasterxml.jackson.annotation.JsonValue; | ||
| import tools.jackson.databind.ObjectMapper; | ||
| import tools.jackson.databind.exc.InvalidFormatException; | ||
| import tools.jackson.databind.json.JsonMapper; | ||
| import tools.jackson.databind.testutil.DatabindTestUtil; | ||
| 
     | 
||
| import org.junit.jupiter.api.Test; | ||
| 
     | 
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||
| 
     | 
||
| // [databind#3580] Enum (de)serialization in conjunction with JsonFormat.Shape.NUMBER_INT | ||
| public class EnumNumberFormatShapeRecord3580Test | ||
| extends DatabindTestUtil | ||
| { | ||
| public record Record3580(@JsonFormat(shape = JsonFormat.Shape.NUMBER) RecordState3580 state) {} | ||
| 
     | 
||
| public enum RecordState3580 { | ||
| OFF(17), | ||
| ON(31), | ||
| UNKNOWN(99); | ||
| 
     | 
||
| private int value; | ||
| 
     | 
||
| RecordState3580(int value) { this.value = value; } | ||
| 
     | 
||
| @JsonValue | ||
| public int value() {return this.value;} | ||
| } | ||
| 
     | 
||
| @Test | ||
| public void testEnumNumberFormatShapeRecord3580() | ||
| throws Exception | ||
| { | ||
| ObjectMapper mapper = JsonMapper.builder().build(); | ||
| 
     | 
||
| // Serialize | ||
| assertEquals("{\"state\":17}", mapper.writeValueAsString(new Record3580(RecordState3580.OFF))); // | ||
| assertEquals("{\"state\":31}", mapper.writeValueAsString(new Record3580(RecordState3580.ON))); // | ||
| assertEquals("{\"state\":99}", mapper.writeValueAsString(new Record3580(RecordState3580.UNKNOWN))); // | ||
| 
     | 
||
| // Pass Deserialize | ||
| assertEquals(RecordState3580.OFF, mapper.readValue("{\"state\":17}", Record3580.class).state); // Pojo[state=OFF] | ||
| assertEquals(RecordState3580.ON, mapper.readValue("{\"state\":31}", Record3580.class).state); // Pojo[state=OFF] | ||
| assertEquals(RecordState3580.UNKNOWN, mapper.readValue("{\"state\":99}", Record3580.class).state); // Pojo[state=OFF] | ||
| 
     | 
||
| // Fail : Try to use ordinal number | ||
| assertThrows(InvalidFormatException.class, () -> mapper.readValue("{\"state\":0}", Record3580.class)); | ||
| } | ||
| } | 
Uh oh!
There was an error while loading. Please reload this page.