diff --git a/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java b/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java index 10289ba9fe..34e1bc4589 100644 --- a/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java +++ b/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java @@ -977,6 +977,17 @@ public String[] findEnumValues(Class enumType, Enum[] enumValues, String[ return names; } + /** + * Finds the Enum value that should be considered the default value, if possible. + * + * @param enumCls The Enum class to scan for the default value. + * @return null if none found or it's not possible to determine one. + * @since 2.8 + */ + public Enum findDefaultEnumValue(Class> enumCls) { + return null; + } + /* /********************************************************** /* Deserialization: general annotations diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java index 53da367364..da74024f4a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java @@ -353,6 +353,18 @@ public enum DeserializationFeature implements ConfigFeature */ READ_UNKNOWN_ENUM_VALUES_AS_NULL(false), + /** + * Feature that allows unknown Enum values to be ignored and a predefined value specified through + * {@link com.fasterxml.jackson.annotation.JsonEnumDefaultValue @JsonEnumDefaultValue} annotation. + * If disabled, unknown Enum values will throw exceptions. + * If enabled, but no predefined default Enum value is specified, an exception will be thrown as well. + *

+ * Feature is disabled by default. + * + * @since 2.8 + */ + READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE(false), + /** * Feature that controls whether numeric timestamp values are expected * to be written using nanosecond timestamps (enabled) or not (disabled), diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java index 13968e9f9e..d506d9c0a7 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java @@ -1917,11 +1917,11 @@ protected EnumResolver constructEnumResolver(Class enumClass, if (config.canOverrideAccessModifiers()) { ClassUtil.checkAndFixAccess(accessor, config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS)); } - return EnumResolver.constructUnsafeUsingMethod(enumClass, accessor); + return EnumResolver.constructUnsafeUsingMethod(enumClass, accessor, config.getAnnotationIntrospector()); } // May need to use Enum.toString() if (config.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING)) { - return EnumResolver.constructUnsafeUsingToString(enumClass); + return EnumResolver.constructUnsafeUsingToString(enumClass, config.getAnnotationIntrospector()); } return EnumResolver.constructUnsafe(enumClass, config.getAnnotationIntrospector()); } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java index b077484dfb..af5dcbf217 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java @@ -28,6 +28,7 @@ public class EnumDeserializer * @since 2.6 */ protected final CompactStringObjectMap _enumLookup; + private final Enum _enumDefaultValue; /** * @since 2.6 @@ -39,6 +40,7 @@ public EnumDeserializer(EnumResolver res) super(res.getEnumClass()); _enumLookup = res.constructLookup(); _enumsByIndex = res.getRawEnums(); + _enumDefaultValue = res.getDefaultValue(); } /** @@ -97,6 +99,9 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx if (index >= 0 && index <= _enumsByIndex.length) { return _enumsByIndex[index]; } + if (ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) && _enumDefaultValue != null) { + return _enumDefaultValue; + } if (!ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)) { throw ctxt.weirdNumberException(index, _enumClass(), "index value outside legal index range [0.."+(_enumsByIndex.length-1)+"]"); @@ -131,6 +136,10 @@ private final Object _deserializeAltString(JsonParser p, DeserializationContext } } } + if (ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + && _enumDefaultValue != null) { + return _enumDefaultValue; + } if (!ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)) { throw ctxt.weirdStringException(name, _enumClass(), "value not one of declared Enum instance names: "+_enumLookup.keys()); diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java index 03659721bb..70878d946e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java @@ -186,6 +186,20 @@ public String[] findEnumValues(Class enumType, Enum[] enumValues, String[] return names; } + /** + * Finds the Enum value that should be considered the default value, if possible. + *

+ * This implementation relies on {@link JsonEnumDefaultValue} annotation to determine the default value if present. + * + * @param enumCls The Enum class to scan for the default value. + * @return null if none found or it's not possible to determine one. + * @since 2.8 + */ + @Override + public Enum findDefaultEnumValue(Class> enumCls) { + return ClassUtil.findFirstAnnotatedEnumValue(enumCls, JsonEnumDefaultValue.class); + } + /* /********************************************************** /* General class annotations diff --git a/src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java b/src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java index 3cdcf700d8..c44a96ca5c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java @@ -842,6 +842,34 @@ public static Class> findEnumType(Class cls) return (Class>) cls; } + /** + * A method that will look for the first Enum value annotated with the given Annotation. + *

+ * If there's more than one value annotated, the first one found will be returned. Which one exactly is used is undetermined. + * + * @param enumClass The Enum class to scan for a value with the given annotation + * @param annotationClass The annotation to look for. + * @return the Enum value annotated with the given Annotation or {@code null} if none is found. + * @throws IllegalArgumentException if there's a reflection issue accessing the Enum + * @since 2.8 + */ + public static Enum findFirstAnnotatedEnumValue(Class> enumClass, Class annotationClass) { + Field[] fields = getDeclaredFields(enumClass); + for (Field field : fields) { + Annotation defaultValueAnnotation = field.getAnnotation(annotationClass); + if (defaultValueAnnotation != null && field.isEnumConstant()) { + try { + Method valueOf = enumClass.getDeclaredMethod("valueOf", String.class); // using `getMethod` causes IllegalAccessException + valueOf.setAccessible(true); + return enumClass.cast(valueOf.invoke(null, field.getName())); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalArgumentException("Could not extract Enum annotated with " + annotationClass.getSimpleName(), e); + } + } + } + return null; + } + /* /********************************************************** /* Jackson-specific stuff diff --git a/src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java b/src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java index 63145541c8..8ee7740b65 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java @@ -11,6 +11,8 @@ */ public class EnumResolver implements java.io.Serializable { + private static final AnnotationIntrospector defaultAnnotationInstrospector = null; + private static final long serialVersionUID = 1L; protected final Class> _enumClass; @@ -19,11 +21,14 @@ public class EnumResolver implements java.io.Serializable protected final HashMap> _enumsById; - protected EnumResolver(Class> enumClass, Enum[] enums, HashMap> map) + protected final Enum _defaultValue; + + protected EnumResolver(Class> enumClass, Enum[] enums, HashMap> map, Enum defaultValue) { _enumClass = enumClass; _enums = enums; _enumsById = map; + _defaultValue = defaultValue; } /** @@ -45,14 +50,28 @@ public static EnumResolver constructFor(Class> enumCls, AnnotationIntros } map.put(name, enumValues[i]); } - return new EnumResolver(enumCls, enumValues, map); + + Enum defaultEnum = ai.findDefaultEnumValue(enumCls); + + return new EnumResolver(enumCls, enumValues, map, defaultEnum); + } + + /** + * @deprecated Since 2.8, use {@link #constructUsingToString(Class, AnnotationIntrospector)} instead + */ + @Deprecated + public static EnumResolver constructUsingToString(Class> enumCls) + { + return constructUsingToString(enumCls, defaultAnnotationInstrospector); } /** * Factory method for constructing resolver that maps from Enum.toString() into * Enum value + * + * @since 2.8 */ - public static EnumResolver constructUsingToString(Class> enumCls) + public static EnumResolver constructUsingToString(Class> enumCls, AnnotationIntrospector ai) { Enum[] enumValues = enumCls.getEnumConstants(); HashMap> map = new HashMap>(); @@ -61,11 +80,23 @@ public static EnumResolver constructUsingToString(Class> enumCls) Enum e = enumValues[i]; map.put(e.toString(), e); } - return new EnumResolver(enumCls, enumValues, map); - } - public static EnumResolver constructUsingMethod(Class> enumCls, - Method accessor) + Enum defaultEnum = ai.findDefaultEnumValue(enumCls); + return new EnumResolver(enumCls, enumValues, map, defaultEnum); + } + + /** + * @deprecated Since 2.8, use {@link #constructUsingMethod(Class, Method, AnnotationIntrospector)} instead + */ + @Deprecated + public static EnumResolver constructUsingMethod(Class> enumCls, Method accessor) { + return constructUsingMethod(enumCls, accessor, defaultAnnotationInstrospector); + } + + /** + * @since 2.8 + */ + public static EnumResolver constructUsingMethod(Class> enumCls, Method accessor, AnnotationIntrospector ai) { Enum[] enumValues = enumCls.getEnumConstants(); HashMap> map = new HashMap>(); @@ -81,7 +112,9 @@ public static EnumResolver constructUsingMethod(Class> enumCls, throw new IllegalArgumentException("Failed to access @JsonValue of Enum value "+en+": "+e.getMessage()); } } - return new EnumResolver(enumCls, enumValues, map); + + Enum defaultEnum = (ai != null) ? ai.findDefaultEnumValue(enumCls) : null; + return new EnumResolver(enumCls, enumValues, map, defaultEnum); } /** @@ -98,34 +131,55 @@ public static EnumResolver constructUnsafe(Class rawEnumCls, AnnotationIntros return constructFor(enumCls, ai); } + /** + * @deprecated Since 2.8, use {@link #constructUnsafeUsingToString(Class, AnnotationIntrospector)} instead + */ + @Deprecated + public static EnumResolver constructUnsafeUsingToString(Class rawEnumCls) + { + return constructUnsafeUsingToString(rawEnumCls, defaultAnnotationInstrospector); + } + /** * Method that needs to be used instead of {@link #constructUsingToString} * if static type of enum is not known. + * + * @since 2.8 */ @SuppressWarnings({ "unchecked" }) - public static EnumResolver constructUnsafeUsingToString(Class rawEnumCls) - { + public static EnumResolver constructUnsafeUsingToString(Class rawEnumCls, AnnotationIntrospector ai) + { // oh so wrong... not much that can be done tho Class> enumCls = (Class>) rawEnumCls; - return constructUsingToString(enumCls); + return constructUsingToString(enumCls, ai); + } + + /** + * @deprecated Since 2.8, use {@link #constructUnsafeUsingMethod(Class, Method, AnnotationIntrospector)} instead. + */ + @Deprecated + public static EnumResolver constructUnsafeUsingMethod(Class rawEnumCls, Method accessor) { + return constructUnsafeUsingMethod(rawEnumCls, accessor, defaultAnnotationInstrospector); } /** * Method used when actual String serialization is indicated using @JsonValue * on a method. + * + * @since 2.8 */ @SuppressWarnings({ "unchecked" }) - public static EnumResolver constructUnsafeUsingMethod(Class rawEnumCls, Method accessor) + public static EnumResolver constructUnsafeUsingMethod(Class rawEnumCls, Method accessor, AnnotationIntrospector ai) { // wrong as ever but: Class> enumCls = (Class>) rawEnumCls; - return constructUsingMethod(enumCls, accessor); + return constructUsingMethod(enumCls, accessor, ai); } public CompactStringObjectMap constructLookup() { return CompactStringObjectMap.construct(_enumsById); } - + public Enum findEnum(String key) { return _enumsById.get(key); } public Enum getEnum(int index) { @@ -135,6 +189,10 @@ public Enum getEnum(int index) { return _enums[index]; } + public Enum getDefaultValue(){ + return _defaultValue; + } + public Enum[] getRawEnums() { return _enums; } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/TestEnumDeserialization.java b/src/test/java/com/fasterxml/jackson/databind/deser/TestEnumDeserialization.java index 9b4c614c48..2743c45410 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/TestEnumDeserialization.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/TestEnumDeserialization.java @@ -167,6 +167,27 @@ public String toString() { ; } + static enum EnumWithDefaultAnno { + A, B, + + @JsonEnumDefaultValue + OTHER; + } + + static enum EnumWithDefaultAnnoAndConstructor { + A, B, + + @JsonEnumDefaultValue + OTHER; + + @JsonCreator public static EnumWithDefaultAnnoAndConstructor fromId(String value) { + for (EnumWithDefaultAnnoAndConstructor e: values()) { + if (e.name().toLowerCase().equals(value)) return e; + } + return null; + } + } + /* /********************************************************** /* Tests @@ -481,4 +502,28 @@ public void testEnumWithJsonPropertyRename() throws Exception assertSame(EnumWithPropertyAnno.B, result[0]); assertSame(EnumWithPropertyAnno.A, result[1]); } + + public void testEnumWithDefaultAnnotation() throws Exception { + final ObjectMapper mapper = new ObjectMapper(); + mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE); + + EnumWithDefaultAnno myEnum = mapper.readValue("\"foo\"", EnumWithDefaultAnno.class); + assertSame(EnumWithDefaultAnno.OTHER, myEnum); + } + + public void testEnumWithDefaultAnnotationUsingIndexes() throws Exception { + final ObjectMapper mapper = new ObjectMapper(); + mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE); + + EnumWithDefaultAnno myEnum = mapper.readValue("9", EnumWithDefaultAnno.class); + assertSame(EnumWithDefaultAnno.OTHER, myEnum); + } + + public void testEnumWithDefaultAnnotationWithConstructor() throws Exception { + final ObjectMapper mapper = new ObjectMapper(); + mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE); + + EnumWithDefaultAnnoAndConstructor myEnum = mapper.readValue("\"foo\"", EnumWithDefaultAnnoAndConstructor.class); + assertNull("When using a constructor, the default value annotation shouldn't be used.", myEnum); + } }