diff --git a/release-notes/VERSION b/release-notes/VERSION index 92309a22c6..4952e0ad70 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -23,6 +23,9 @@ Versions: 3.x (for earlier see VERSION-2.x) (reported by Phil C) #5354: jackson-databind 3.0.0 renders `callbacks` property from Spring CGLIB Proxy (reported, fix contributed by Rob W) +#3580: Fix Enum (de)serialization in conjunction with `JsonFormat.Shape.NUMBER_INT` + (reported by @kistlers) + (fixed by Joo-Hyuk K) 3.0.0 (03-Oct-2025) diff --git a/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java index 239d5dcf39..8dfd12ae86 100644 --- a/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/tools/jackson/databind/deser/BasicDeserializerFactory.java @@ -1099,7 +1099,8 @@ public ValueDeserializer> createEnumDeserializer(DeserializationContext ctxt, config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS), constructEnumNamingStrategyResolver(config, beanDescRef.getClassInfo()), // since 2.16 - EnumResolver.constructUsingToString(config, beanDescRef.getClassInfo()) + EnumResolver.constructUsingToString(config, beanDescRef.getClassInfo()), + constructEnumResolverNumberShape(enumClass, config, beanDescRef.get()) ); } } @@ -1728,6 +1729,23 @@ protected EnumResolver constructEnumNamingStrategyResolver(DeserializationConfig : EnumResolver.constructUsingEnumNamingStrategy(config, enumClass, enumNamingStrategy); } + /** + * @since 3.1.0 + */ + protected EnumResolver constructEnumResolverNumberShape(Class> enumClass, + DeserializationConfig config, BeanDescription beanDesc) + { + AnnotatedMember jvAcc = beanDesc.findJsonValueAccessor(); + if (jvAcc != null) { + if (config.canOverrideAccessModifiers()) { + ClassUtil.checkAndFixAccess(jvAcc.getMember(), + config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS)); + } + return EnumResolver.constructUsingNumberShape(config, beanDesc.getClassInfo(), jvAcc); + } + return null; + } + protected boolean _hasCreatorAnnotation(MapperConfig> config, Annotated ann) { AnnotationIntrospector intr = config.getAnnotationIntrospector(); diff --git a/src/main/java/tools/jackson/databind/deser/jdk/EnumDeserializer.java b/src/main/java/tools/jackson/databind/deser/jdk/EnumDeserializer.java index 95daa903ad..3c6e3d37d4 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/EnumDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/EnumDeserializer.java @@ -65,8 +65,30 @@ public class EnumDeserializer */ protected final CompactStringObjectMap _lookupByEnumNaming; + /** + * We may also have integer-type of representation for Enum's, along with `@JsonValue`. + * + * @since 3.1 + */ + protected final CompactStringObjectMap _lookupByShapeNumberInt; + + /** + * Flag to check if FormatShape of int number type would be used to deserialize + */ + protected final boolean _isShapeNumberInt; + + @Deprecated // since 3.1.0 public EnumDeserializer(EnumResolver byNameResolver, boolean caseInsensitive, - EnumResolver byEnumNamingResolver, EnumResolver toStringResolver) + EnumResolver byEnumNamingResolver, EnumResolver toStringResolver) + { + this(byNameResolver, caseInsensitive, byEnumNamingResolver, toStringResolver, null); + } + + /** + * @since 3.1.0 + */ + public EnumDeserializer(EnumResolver byNameResolver, boolean caseInsensitive, + EnumResolver byEnumNamingResolver, EnumResolver toStringResolver, EnumResolver shapeNumberResolver) { super(byNameResolver.getEnumClass()); _lookupByName = byNameResolver.constructLookup(); @@ -75,8 +97,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 +113,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 +221,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 +269,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 +278,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 +294,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); } diff --git a/src/main/java/tools/jackson/databind/util/EnumResolver.java b/src/main/java/tools/jackson/databind/util/EnumResolver.java index 0c9a1bc08a..a6b6587871 100644 --- a/src/main/java/tools/jackson/databind/util/EnumResolver.java +++ b/src/main/java/tools/jackson/databind/util/EnumResolver.java @@ -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 + *
+     * 1. actual String serialization is indicated using @JsonValue on a method in Enum class AND
+     * 2. Enum class is annotated with `@JsonFormat`
+     *
+     */
+    public static EnumResolver constructUsingNumberShape(DeserializationConfig config, AnnotatedClass annotatedClass, AnnotatedMember accessor)
+    {
+        // prepare data
+        final boolean isIgnoreCase = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
+        final Class> enumCls0 = annotatedClass.getRawType();
+        final Class