Skip to content

Commit

Permalink
Added feature to allow deserialization of unknown Enums using a prede…
Browse files Browse the repository at this point in the history
…fined value.
  • Loading branch information
Alejandro Rivera committed Feb 12, 2016
1 parent 5235ba8 commit a0f36f3
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 5 deletions.
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 @@ -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
@@ -1,7 +1,11 @@
package com.fasterxml.jackson.databind.util;

import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.AnnotationIntrospector;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

Expand All @@ -19,11 +23,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,7 +52,29 @@ public static EnumResolver constructFor(Class<Enum<?>> enumCls, AnnotationIntros
}
map.put(name, enumValues[i]);
}
return new EnumResolver(enumCls, enumValues, map);

Enum<?> defaultEnum = determineDefaultEnumValue(enumCls);

return new EnumResolver(enumCls, enumValues, map, defaultEnum);
}

private static Enum<?> determineDefaultEnumValue(Class<Enum<?>> enumCls) {
Field[] fields = ClassUtil.getDeclaredFields(enumCls);
for (Field field : fields) {
JsonEnumDefaultValue defaultValueAnnotation = field.getAnnotation(JsonEnumDefaultValue.class);
if (defaultValueAnnotation != null && field.isEnumConstant()) {
try {
Method valueOf = enumCls.getDeclaredMethod("valueOf", String.class); // using `getMethod` causes IllegalAccessException
valueOf.setAccessible(true);
return enumCls.cast(valueOf.invoke(null, field.getName()));
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
java.util.logging.Logger.getLogger(EnumResolver.class.getName())
.warning("Could not extract Enum annotated with " + JsonEnumDefaultValue.class.getSimpleName()
+ " due to: " + e.getLocalizedMessage());
}
}
}
return null;
}

/**
Expand All @@ -61,7 +90,8 @@ public static EnumResolver constructUsingToString(Class<Enum<?>> enumCls)
Enum<?> e = enumValues[i];
map.put(e.toString(), e);
}
return new EnumResolver(enumCls, enumValues, map);
Enum<?> defaultValue = determineDefaultEnumValue(enumCls);
return new EnumResolver(enumCls, enumValues, map, defaultValue);
}

public static EnumResolver constructUsingMethod(Class<Enum<?>> enumCls,
Expand All @@ -81,7 +111,10 @@ 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<?> defaultValue = determineDefaultEnumValue(enumCls);

return new EnumResolver(enumCls, enumValues, map, defaultValue);
}

/**
Expand Down Expand Up @@ -125,7 +158,7 @@ public static EnumResolver constructUnsafeUsingMethod(Class<?> rawEnumCls, Metho
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 +168,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 a0f36f3

Please sign in to comment.