Skip to content

Commit

Permalink
Merge pull request #1126 from AlejandroRivera/feature/deserialize-unk…
Browse files Browse the repository at this point in the history
…nown-enums-using-default

Allow deserialization of unknown Enums using a predefined value
  • Loading branch information
cowtowncoder committed Feb 23, 2016
2 parents aef0d77 + 7e6ba9e commit cfd893e
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<Enum<?>> enumCls) {
return null;
}

/*
/**********************************************************
/* Deserialization: general annotations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*<p>
* 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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class EnumDeserializer
* @since 2.6
*/
protected final CompactStringObjectMap _enumLookup;
private final Enum<?> _enumDefaultValue;

/**
* @since 2.6
Expand All @@ -39,6 +40,7 @@ public EnumDeserializer(EnumResolver res)
super(res.getEnumClass());
_enumLookup = res.constructLookup();
_enumsByIndex = res.getRawEnums();
_enumDefaultValue = res.getDefaultValue();
}

/**
Expand Down Expand Up @@ -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)+"]");
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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<Enum<?>> enumCls) {
return ClassUtil.findFirstAnnotatedEnumValue(enumCls, JsonEnumDefaultValue.class);
}

/*
/**********************************************************
/* General class annotations
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,34 @@ public static Class<? extends Enum<?>> findEnumType(Class<?> cls)
return (Class<? extends Enum<?>>) cls;
}

/**
* A method that will look for the first Enum value annotated with the given Annotation.
* <p>
* 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 <T extends Annotation> Enum<?> findFirstAnnotatedEnumValue(Class<Enum<?>> enumClass, Class<T> 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
Expand Down
86 changes: 72 additions & 14 deletions src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Enum<?>> _enumClass;
Expand All @@ -19,11 +21,14 @@ public class EnumResolver implements java.io.Serializable

protected final HashMap<String, Enum<?>> _enumsById;

protected EnumResolver(Class<Enum<?>> enumClass, Enum<?>[] enums, HashMap<String, Enum<?>> map)
protected final Enum<?> _defaultValue;

protected EnumResolver(Class<Enum<?>> enumClass, Enum<?>[] enums, HashMap<String, Enum<?>> map, Enum<?> defaultValue)
{
_enumClass = enumClass;
_enums = enums;
_enumsById = map;
_defaultValue = defaultValue;
}

/**
Expand All @@ -45,14 +50,28 @@ public static EnumResolver constructFor(Class<Enum<?>> 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<Enum<?>> 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<Enum<?>> enumCls)
public static EnumResolver constructUsingToString(Class<Enum<?>> enumCls, AnnotationIntrospector ai)
{
Enum<?>[] enumValues = enumCls.getEnumConstants();
HashMap<String, Enum<?>> map = new HashMap<String, Enum<?>>();
Expand All @@ -61,11 +80,23 @@ public static EnumResolver constructUsingToString(Class<Enum<?>> enumCls)
Enum<?> e = enumValues[i];
map.put(e.toString(), e);
}
return new EnumResolver(enumCls, enumValues, map);
}

public static EnumResolver constructUsingMethod(Class<Enum<?>> 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<Enum<?>> enumCls, Method accessor) {
return constructUsingMethod(enumCls, accessor, defaultAnnotationInstrospector);
}

/**
* @since 2.8
*/
public static EnumResolver constructUsingMethod(Class<Enum<?>> enumCls, Method accessor, AnnotationIntrospector ai)
{
Enum<?>[] enumValues = enumCls.getEnumConstants();
HashMap<String, Enum<?>> map = new HashMap<String, Enum<?>>();
Expand All @@ -81,7 +112,9 @@ public static EnumResolver constructUsingMethod(Class<Enum<?>> 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);
}

/**
Expand All @@ -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<Enum<?>> enumCls = (Class<Enum<?>>) 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<Enum<?>> enumCls = (Class<Enum<?>>) 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) {
Expand All @@ -135,6 +189,10 @@ public Enum<?> getEnum(int index) {
return _enums[index];
}

public Enum<?> getDefaultValue(){
return _defaultValue;
}

public Enum<?>[] getRawEnums() {
return _enums;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}

0 comments on commit cfd893e

Please sign in to comment.