Skip to content
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

Allow deserialization of unknown Enums using a predefined value #1126

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -842,6 +842,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);
}
}