Skip to content

Commit cfd893e

Browse files
committed
Merge pull request #1126 from AlejandroRivera/feature/deserialize-unknown-enums-using-default
Allow deserialization of unknown Enums using a predefined value
2 parents aef0d77 + 7e6ba9e commit cfd893e

File tree

8 files changed

+193
-16
lines changed

8 files changed

+193
-16
lines changed

src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,17 @@ public String[] findEnumValues(Class<?> enumType, Enum<?>[] enumValues, String[
977977
return names;
978978
}
979979

980+
/**
981+
* Finds the Enum value that should be considered the default value, if possible.
982+
*
983+
* @param enumCls The Enum class to scan for the default value.
984+
* @return null if none found or it's not possible to determine one.
985+
* @since 2.8
986+
*/
987+
public Enum<?> findDefaultEnumValue(Class<Enum<?>> enumCls) {
988+
return null;
989+
}
990+
980991
/*
981992
/**********************************************************
982993
/* Deserialization: general annotations

src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,18 @@ public enum DeserializationFeature implements ConfigFeature
353353
*/
354354
READ_UNKNOWN_ENUM_VALUES_AS_NULL(false),
355355

356+
/**
357+
* Feature that allows unknown Enum values to be ignored and a predefined value specified through
358+
* {@link com.fasterxml.jackson.annotation.JsonEnumDefaultValue @JsonEnumDefaultValue} annotation.
359+
* If disabled, unknown Enum values will throw exceptions.
360+
* If enabled, but no predefined default Enum value is specified, an exception will be thrown as well.
361+
*<p>
362+
* Feature is disabled by default.
363+
*
364+
* @since 2.8
365+
*/
366+
READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE(false),
367+
356368
/**
357369
* Feature that controls whether numeric timestamp values are expected
358370
* to be written using nanosecond timestamps (enabled) or not (disabled),

src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1917,11 +1917,11 @@ protected EnumResolver constructEnumResolver(Class<?> enumClass,
19171917
if (config.canOverrideAccessModifiers()) {
19181918
ClassUtil.checkAndFixAccess(accessor, config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
19191919
}
1920-
return EnumResolver.constructUnsafeUsingMethod(enumClass, accessor);
1920+
return EnumResolver.constructUnsafeUsingMethod(enumClass, accessor, config.getAnnotationIntrospector());
19211921
}
19221922
// May need to use Enum.toString()
19231923
if (config.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING)) {
1924-
return EnumResolver.constructUnsafeUsingToString(enumClass);
1924+
return EnumResolver.constructUnsafeUsingToString(enumClass, config.getAnnotationIntrospector());
19251925
}
19261926
return EnumResolver.constructUnsafe(enumClass, config.getAnnotationIntrospector());
19271927
}

src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class EnumDeserializer
2828
* @since 2.6
2929
*/
3030
protected final CompactStringObjectMap _enumLookup;
31+
private final Enum<?> _enumDefaultValue;
3132

3233
/**
3334
* @since 2.6
@@ -39,6 +40,7 @@ public EnumDeserializer(EnumResolver res)
3940
super(res.getEnumClass());
4041
_enumLookup = res.constructLookup();
4142
_enumsByIndex = res.getRawEnums();
43+
_enumDefaultValue = res.getDefaultValue();
4244
}
4345

4446
/**
@@ -97,6 +99,9 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
9799
if (index >= 0 && index <= _enumsByIndex.length) {
98100
return _enumsByIndex[index];
99101
}
102+
if (ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) && _enumDefaultValue != null) {
103+
return _enumDefaultValue;
104+
}
100105
if (!ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)) {
101106
throw ctxt.weirdNumberException(index, _enumClass(),
102107
"index value outside legal index range [0.."+(_enumsByIndex.length-1)+"]");
@@ -131,6 +136,10 @@ private final Object _deserializeAltString(JsonParser p, DeserializationContext
131136
}
132137
}
133138
}
139+
if (ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
140+
&& _enumDefaultValue != null) {
141+
return _enumDefaultValue;
142+
}
134143
if (!ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)) {
135144
throw ctxt.weirdStringException(name, _enumClass(),
136145
"value not one of declared Enum instance names: "+_enumLookup.keys());

src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,20 @@ public String[] findEnumValues(Class<?> enumType, Enum<?>[] enumValues, String[]
186186
return names;
187187
}
188188

189+
/**
190+
* Finds the Enum value that should be considered the default value, if possible.
191+
* <p>
192+
* This implementation relies on {@link JsonEnumDefaultValue} annotation to determine the default value if present.
193+
*
194+
* @param enumCls The Enum class to scan for the default value.
195+
* @return null if none found or it's not possible to determine one.
196+
* @since 2.8
197+
*/
198+
@Override
199+
public Enum<?> findDefaultEnumValue(Class<Enum<?>> enumCls) {
200+
return ClassUtil.findFirstAnnotatedEnumValue(enumCls, JsonEnumDefaultValue.class);
201+
}
202+
189203
/*
190204
/**********************************************************
191205
/* General class annotations

src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,34 @@ public static Class<? extends Enum<?>> findEnumType(Class<?> cls)
863863
return (Class<? extends Enum<?>>) cls;
864864
}
865865

866+
/**
867+
* A method that will look for the first Enum value annotated with the given Annotation.
868+
* <p>
869+
* If there's more than one value annotated, the first one found will be returned. Which one exactly is used is undetermined.
870+
*
871+
* @param enumClass The Enum class to scan for a value with the given annotation
872+
* @param annotationClass The annotation to look for.
873+
* @return the Enum value annotated with the given Annotation or {@code null} if none is found.
874+
* @throws IllegalArgumentException if there's a reflection issue accessing the Enum
875+
* @since 2.8
876+
*/
877+
public static <T extends Annotation> Enum<?> findFirstAnnotatedEnumValue(Class<Enum<?>> enumClass, Class<T> annotationClass) {
878+
Field[] fields = getDeclaredFields(enumClass);
879+
for (Field field : fields) {
880+
Annotation defaultValueAnnotation = field.getAnnotation(annotationClass);
881+
if (defaultValueAnnotation != null && field.isEnumConstant()) {
882+
try {
883+
Method valueOf = enumClass.getDeclaredMethod("valueOf", String.class); // using `getMethod` causes IllegalAccessException
884+
valueOf.setAccessible(true);
885+
return enumClass.cast(valueOf.invoke(null, field.getName()));
886+
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
887+
throw new IllegalArgumentException("Could not extract Enum annotated with " + annotationClass.getSimpleName(), e);
888+
}
889+
}
890+
}
891+
return null;
892+
}
893+
866894
/*
867895
/**********************************************************
868896
/* Jackson-specific stuff

src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
*/
1212
public class EnumResolver implements java.io.Serializable
1313
{
14+
private static final AnnotationIntrospector defaultAnnotationInstrospector = null;
15+
1416
private static final long serialVersionUID = 1L;
1517

1618
protected final Class<Enum<?>> _enumClass;
@@ -19,11 +21,14 @@ public class EnumResolver implements java.io.Serializable
1921

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

22-
protected EnumResolver(Class<Enum<?>> enumClass, Enum<?>[] enums, HashMap<String, Enum<?>> map)
24+
protected final Enum<?> _defaultValue;
25+
26+
protected EnumResolver(Class<Enum<?>> enumClass, Enum<?>[] enums, HashMap<String, Enum<?>> map, Enum<?> defaultValue)
2327
{
2428
_enumClass = enumClass;
2529
_enums = enums;
2630
_enumsById = map;
31+
_defaultValue = defaultValue;
2732
}
2833

2934
/**
@@ -45,14 +50,28 @@ public static EnumResolver constructFor(Class<Enum<?>> enumCls, AnnotationIntros
4550
}
4651
map.put(name, enumValues[i]);
4752
}
48-
return new EnumResolver(enumCls, enumValues, map);
53+
54+
Enum<?> defaultEnum = ai.findDefaultEnumValue(enumCls);
55+
56+
return new EnumResolver(enumCls, enumValues, map, defaultEnum);
57+
}
58+
59+
/**
60+
* @deprecated Since 2.8, use {@link #constructUsingToString(Class, AnnotationIntrospector)} instead
61+
*/
62+
@Deprecated
63+
public static EnumResolver constructUsingToString(Class<Enum<?>> enumCls)
64+
{
65+
return constructUsingToString(enumCls, defaultAnnotationInstrospector);
4966
}
5067

5168
/**
5269
* Factory method for constructing resolver that maps from Enum.toString() into
5370
* Enum value
71+
*
72+
* @since 2.8
5473
*/
55-
public static EnumResolver constructUsingToString(Class<Enum<?>> enumCls)
74+
public static EnumResolver constructUsingToString(Class<Enum<?>> enumCls, AnnotationIntrospector ai)
5675
{
5776
Enum<?>[] enumValues = enumCls.getEnumConstants();
5877
HashMap<String, Enum<?>> map = new HashMap<String, Enum<?>>();
@@ -61,11 +80,23 @@ public static EnumResolver constructUsingToString(Class<Enum<?>> enumCls)
6180
Enum<?> e = enumValues[i];
6281
map.put(e.toString(), e);
6382
}
64-
return new EnumResolver(enumCls, enumValues, map);
65-
}
6683

67-
public static EnumResolver constructUsingMethod(Class<Enum<?>> enumCls,
68-
Method accessor)
84+
Enum<?> defaultEnum = ai.findDefaultEnumValue(enumCls);
85+
return new EnumResolver(enumCls, enumValues, map, defaultEnum);
86+
}
87+
88+
/**
89+
* @deprecated Since 2.8, use {@link #constructUsingMethod(Class, Method, AnnotationIntrospector)} instead
90+
*/
91+
@Deprecated
92+
public static EnumResolver constructUsingMethod(Class<Enum<?>> enumCls, Method accessor) {
93+
return constructUsingMethod(enumCls, accessor, defaultAnnotationInstrospector);
94+
}
95+
96+
/**
97+
* @since 2.8
98+
*/
99+
public static EnumResolver constructUsingMethod(Class<Enum<?>> enumCls, Method accessor, AnnotationIntrospector ai)
69100
{
70101
Enum<?>[] enumValues = enumCls.getEnumConstants();
71102
HashMap<String, Enum<?>> map = new HashMap<String, Enum<?>>();
@@ -81,7 +112,9 @@ public static EnumResolver constructUsingMethod(Class<Enum<?>> enumCls,
81112
throw new IllegalArgumentException("Failed to access @JsonValue of Enum value "+en+": "+e.getMessage());
82113
}
83114
}
84-
return new EnumResolver(enumCls, enumValues, map);
115+
116+
Enum<?> defaultEnum = (ai != null) ? ai.findDefaultEnumValue(enumCls) : null;
117+
return new EnumResolver(enumCls, enumValues, map, defaultEnum);
85118
}
86119

87120
/**
@@ -98,34 +131,55 @@ public static EnumResolver constructUnsafe(Class<?> rawEnumCls, AnnotationIntros
98131
return constructFor(enumCls, ai);
99132
}
100133

134+
/**
135+
* @deprecated Since 2.8, use {@link #constructUnsafeUsingToString(Class, AnnotationIntrospector)} instead
136+
*/
137+
@Deprecated
138+
public static EnumResolver constructUnsafeUsingToString(Class<?> rawEnumCls)
139+
{
140+
return constructUnsafeUsingToString(rawEnumCls, defaultAnnotationInstrospector);
141+
}
142+
101143
/**
102144
* Method that needs to be used instead of {@link #constructUsingToString}
103145
* if static type of enum is not known.
146+
*
147+
* @since 2.8
104148
*/
105149
@SuppressWarnings({ "unchecked" })
106-
public static EnumResolver constructUnsafeUsingToString(Class<?> rawEnumCls)
107-
{
150+
public static EnumResolver constructUnsafeUsingToString(Class<?> rawEnumCls, AnnotationIntrospector ai)
151+
{
108152
// oh so wrong... not much that can be done tho
109153
Class<Enum<?>> enumCls = (Class<Enum<?>>) rawEnumCls;
110-
return constructUsingToString(enumCls);
154+
return constructUsingToString(enumCls, ai);
155+
}
156+
157+
/**
158+
* @deprecated Since 2.8, use {@link #constructUnsafeUsingMethod(Class, Method, AnnotationIntrospector)} instead.
159+
*/
160+
@Deprecated
161+
public static EnumResolver constructUnsafeUsingMethod(Class<?> rawEnumCls, Method accessor) {
162+
return constructUnsafeUsingMethod(rawEnumCls, accessor, defaultAnnotationInstrospector);
111163
}
112164

113165
/**
114166
* Method used when actual String serialization is indicated using @JsonValue
115167
* on a method.
168+
*
169+
* @since 2.8
116170
*/
117171
@SuppressWarnings({ "unchecked" })
118-
public static EnumResolver constructUnsafeUsingMethod(Class<?> rawEnumCls, Method accessor)
172+
public static EnumResolver constructUnsafeUsingMethod(Class<?> rawEnumCls, Method accessor, AnnotationIntrospector ai)
119173
{
120174
// wrong as ever but:
121175
Class<Enum<?>> enumCls = (Class<Enum<?>>) rawEnumCls;
122-
return constructUsingMethod(enumCls, accessor);
176+
return constructUsingMethod(enumCls, accessor, ai);
123177
}
124178

125179
public CompactStringObjectMap constructLookup() {
126180
return CompactStringObjectMap.construct(_enumsById);
127181
}
128-
182+
129183
public Enum<?> findEnum(String key) { return _enumsById.get(key); }
130184

131185
public Enum<?> getEnum(int index) {
@@ -135,6 +189,10 @@ public Enum<?> getEnum(int index) {
135189
return _enums[index];
136190
}
137191

192+
public Enum<?> getDefaultValue(){
193+
return _defaultValue;
194+
}
195+
138196
public Enum<?>[] getRawEnums() {
139197
return _enums;
140198
}

src/test/java/com/fasterxml/jackson/databind/deser/TestEnumDeserialization.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,27 @@ public String toString() {
167167
;
168168
}
169169

170+
static enum EnumWithDefaultAnno {
171+
A, B,
172+
173+
@JsonEnumDefaultValue
174+
OTHER;
175+
}
176+
177+
static enum EnumWithDefaultAnnoAndConstructor {
178+
A, B,
179+
180+
@JsonEnumDefaultValue
181+
OTHER;
182+
183+
@JsonCreator public static EnumWithDefaultAnnoAndConstructor fromId(String value) {
184+
for (EnumWithDefaultAnnoAndConstructor e: values()) {
185+
if (e.name().toLowerCase().equals(value)) return e;
186+
}
187+
return null;
188+
}
189+
}
190+
170191
/*
171192
/**********************************************************
172193
/* Tests
@@ -481,4 +502,28 @@ public void testEnumWithJsonPropertyRename() throws Exception
481502
assertSame(EnumWithPropertyAnno.B, result[0]);
482503
assertSame(EnumWithPropertyAnno.A, result[1]);
483504
}
505+
506+
public void testEnumWithDefaultAnnotation() throws Exception {
507+
final ObjectMapper mapper = new ObjectMapper();
508+
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);
509+
510+
EnumWithDefaultAnno myEnum = mapper.readValue("\"foo\"", EnumWithDefaultAnno.class);
511+
assertSame(EnumWithDefaultAnno.OTHER, myEnum);
512+
}
513+
514+
public void testEnumWithDefaultAnnotationUsingIndexes() throws Exception {
515+
final ObjectMapper mapper = new ObjectMapper();
516+
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);
517+
518+
EnumWithDefaultAnno myEnum = mapper.readValue("9", EnumWithDefaultAnno.class);
519+
assertSame(EnumWithDefaultAnno.OTHER, myEnum);
520+
}
521+
522+
public void testEnumWithDefaultAnnotationWithConstructor() throws Exception {
523+
final ObjectMapper mapper = new ObjectMapper();
524+
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);
525+
526+
EnumWithDefaultAnnoAndConstructor myEnum = mapper.readValue("\"foo\"", EnumWithDefaultAnnoAndConstructor.class);
527+
assertNull("When using a constructor, the default value annotation shouldn't be used.", myEnum);
528+
}
484529
}

0 commit comments

Comments
 (0)