package com.fasterxml.jackson.databind.deser.jdk;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.std.FromStringDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
@SuppressWarnings("serial")
public class EnumDeserializationTest
extends BaseMapTest
{
enum TestEnum { JACKSON, RULES, OK; }
/**
* Alternative version that annotates which deserializer to use
*/
@JsonDeserialize(using=DummyDeserializer.class)
enum AnnotatedTestEnum {
JACKSON, RULES, OK;
}
public static class DummyDeserializer extends StdDeserializer<Object>
{
public DummyDeserializer() { super(Object.class); }
@Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt)
{
return AnnotatedTestEnum.OK;
}
}
public static class LcEnumDeserializer extends StdDeserializer<TestEnum>
{
public LcEnumDeserializer() { super(TestEnum.class); }
@Override
public TestEnum deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException
{
return TestEnum.valueOf(jp.getText().toUpperCase());
}
}
protected enum LowerCaseEnum {
A, B, C;
private LowerCaseEnum() { }
@Override
public String toString() { return name().toLowerCase(); }
}
protected enum EnumWithJsonValue {
A("foo"), B("bar");
private final String name;
private EnumWithJsonValue(String n) {
name = n;
}
@JsonValue
@Override
public String toString() { return name; }
}
static class ClassWithEnumMapKey {
@JsonProperty Map<TestEnum, String> map;
}
// [databind#677]
static enum EnumWithPropertyAnno {
@JsonProperty("a")
A,
// For this value, force use of anonymous sub-class, to ensure things still work
@JsonProperty("b")
B {
@Override
public String toString() {
return "bb";
}
}
;
}
// [databind#1161]
enum Enum1161 {
A, B, C;
@Override
public String toString() {
return name().toLowerCase();
};
}
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;
}
}
// //
public enum AnEnum {
ZERO,
ONE
}
public static class AnEnumDeserializer extends FromStringDeserializer<AnEnum> {
public AnEnumDeserializer() {
super(AnEnum.class);
}
@Override
protected AnEnum _deserialize(String value, DeserializationContext ctxt) throws IOException {
try {
return AnEnum.valueOf(value);
} catch (IllegalArgumentException e) {
return (AnEnum) ctxt.handleWeirdStringValue(AnEnum.class, value,
"Undefined AnEnum code");
}
}
}
public static class AnEnumKeyDeserializer extends KeyDeserializer {
@Override
public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException {
try {
return AnEnum.valueOf(key);
} catch (IllegalArgumentException e) {
return ctxt.handleWeirdKey(AnEnum.class, key, "Undefined AnEnum code");
}
}
}
@JsonDeserialize(using = AnEnumDeserializer.class, keyUsing = AnEnumKeyDeserializer.class)
public enum LanguageCodeMixin {
}
public static class EnumModule extends SimpleModule {
@Override
public void setupModule(SetupContext context) {
context.setMixInAnnotations(AnEnum.class, LanguageCodeMixin.class);
}
public static ObjectMapper setupObjectMapper(ObjectMapper mapper) {
final EnumModule module = new EnumModule();
mapper.registerModule(module);
return mapper;
}
}
/*
/**********************************************************
/* Test methods
/**********************************************************
*/
protected final ObjectMapper MAPPER = new ObjectMapper();
public void testSimple() throws Exception
{
// First "good" case with Strings
String JSON = "\"OK\" \"RULES\" null";
// multiple main-level mappings, need explicit parser:
JsonParser jp = MAPPER.getFactory().createParser(JSON);
assertEquals(TestEnum.OK, MAPPER.readValue(jp, TestEnum.class));
assertEquals(TestEnum.RULES, MAPPER.readValue(jp, TestEnum.class));
/* should be ok; nulls are typeless; handled by mapper, not by
* deserializer
*/
assertNull(MAPPER.readValue(jp, TestEnum.class));
// and no more content beyond that...
assertFalse(jp.hasCurrentToken());
// Then alternative with index (0 means first entry)
assertEquals(TestEnum.JACKSON, MAPPER.readValue(" 0 ", TestEnum.class));
// Then error case: unrecognized value
try {
/*Object result =*/ MAPPER.readValue("\"NO-SUCH-VALUE\"", TestEnum.class);
fail("Expected an exception for bogus enum value...");
} catch (JsonMappingException jex) {
verifyException(jex, "value not one of declared");
}
jp.close();
}
/**
* Enums are considered complex if they have code (and hence sub-classes)... an
* example is TimeUnit
*/
public void testComplexEnum() throws Exception
{
String json = MAPPER.writeValueAsString(TimeUnit.SECONDS);
assertEquals(quote("SECONDS"), json);
TimeUnit result = MAPPER.readValue(json, TimeUnit.class);
assertSame(TimeUnit.SECONDS, result);
}
/**
* Testing to see that annotation override works
*/
public void testAnnotated() throws Exception
{
AnnotatedTestEnum e = MAPPER.readValue("\"JACKSON\"", AnnotatedTestEnum.class);
/* dummy deser always returns value OK, independent of input;
* only works if annotation is used
*/
assertEquals(AnnotatedTestEnum.OK, e);
}
public void testSubclassedEnums() throws Exception
{
EnumWithSubClass value = MAPPER.readValue("\"A\"", EnumWithSubClass.class);
assertEquals(EnumWithSubClass.A, value);
}
public void testToStringEnums() throws Exception
{
// can't reuse global one due to reconfig
ObjectMapper m = new ObjectMapper();
m.configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, true);
LowerCaseEnum value = m.readValue("\"c\"", LowerCaseEnum.class);
assertEquals(LowerCaseEnum.C, value);
}
public void testNumbersToEnums() throws Exception
{
// by default numbers are fine:
assertFalse(MAPPER.getDeserializationConfig().isEnabled(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS));
TestEnum value = MAPPER.readValue("1", TestEnum.class);
assertSame(TestEnum.RULES, value);
// but can also be changed to errors:
ObjectReader r = MAPPER.readerFor(TestEnum.class)
.with(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS);
try {
value = r.readValue("1");
fail("Expected an error");
} catch (JsonMappingException e) {
verifyException(e, "Can not deserialize");
verifyException(e, "not allowed to deserialize Enum value out of number: disable");
}
// and [databind#684]
try {
value = r.readValue(quote("1"));
fail("Expected an error");
} catch (JsonMappingException e) {
verifyException(e, "Can not deserialize");
// 26-Jan-2017, tatu: as per [databind#1505], should fail bit differently
verifyException(e, "value not one of declared Enum");
}
}
public void testEnumsWithIndex() throws Exception
{
ObjectMapper m = new ObjectMapper();
m.enable(SerializationFeature.WRITE_ENUMS_USING_INDEX);
String json = m.writeValueAsString(TestEnum.RULES);
assertEquals(String.valueOf(TestEnum.RULES.ordinal()), json);
TestEnum result = m.readValue(json, TestEnum.class);
assertSame(TestEnum.RULES, result);
}
public void testEnumsWithJsonValue() throws Exception
{
// first, enum as is
EnumWithJsonValue e = MAPPER.readValue(quote("foo"), EnumWithJsonValue.class);
assertSame(EnumWithJsonValue.A, e);
e = MAPPER.readValue(quote("bar"), EnumWithJsonValue.class);
assertSame(EnumWithJsonValue.B, e);
// then in EnumSet
EnumSet<EnumWithJsonValue> set = MAPPER.readValue("[\"bar\"]",
new TypeReference<EnumSet<EnumWithJsonValue>>() { });
assertNotNull(set);
assertEquals(1, set.size());
assertTrue(set.contains(EnumWithJsonValue.B));
assertFalse(set.contains(EnumWithJsonValue.A));
// and finally EnumMap
EnumMap<EnumWithJsonValue,Integer> map = MAPPER.readValue("{\"foo\":13}",
new TypeReference<EnumMap<EnumWithJsonValue, Integer>>() { });
assertNotNull(map);
assertEquals(1, map.size());
assertEquals(Integer.valueOf(13), map.get(EnumWithJsonValue.A));
}
// Ability to ignore unknown Enum values:
public void testAllowUnknownEnumValuesReadAsNull() throws Exception
{
// can not use shared mapper when changing configs...
ObjectReader reader = MAPPER.reader(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
assertNull(reader.forType(TestEnum.class).readValue("\"NO-SUCH-VALUE\""));
assertNull(reader.forType(TestEnum.class).readValue(" 4343 "));
}
public void testAllowUnknownEnumValuesForEnumSets() throws Exception
{
ObjectReader reader = MAPPER.reader(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
EnumSet<TestEnum> result = reader.forType(new TypeReference<EnumSet<TestEnum>>() { })
.readValue("[\"NO-SUCH-VALUE\"]");
assertEquals(0, result.size());
}
public void testAllowUnknownEnumValuesAsMapKeysReadAsNull() throws Exception
{
ObjectReader reader = MAPPER.reader(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);
ClassWithEnumMapKey result = reader.forType(ClassWithEnumMapKey.class)
.readValue("{\"map\":{\"NO-SUCH-VALUE\":\"val\"}}");
assertTrue(result.map.containsKey(null));
}
public void testDoNotAllowUnknownEnumValuesAsMapKeysWhenReadAsNullDisabled() throws Exception
{
assertFalse(MAPPER.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL));
try {
MAPPER.readValue("{\"map\":{\"NO-SUCH-VALUE\":\"val\"}}", ClassWithEnumMapKey.class);
fail("Expected an exception for bogus enum value...");
} catch (JsonMappingException jex) {
verifyException(jex, "Can not deserialize Map key of type com.fasterxml.jackson.databind.deser");
}
}
// [databind#141]: allow mapping of empty String into null
public void testEnumsWithEmpty() throws Exception
{
final ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
TestEnum result = mapper.readValue("\"\"", TestEnum.class);
assertNull(result);
}
public void testGenericEnumDeserialization() throws Exception
{
final ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("foobar");
module.addDeserializer(Enum.class, new LcEnumDeserializer());
mapper.registerModule(module);
// not sure this is totally safe but...
assertEquals(TestEnum.JACKSON, mapper.readValue(quote("jackson"), TestEnum.class));
}
// [databind#381]
public void testUnwrappedEnum() throws Exception {
final ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS);
assertEquals(TestEnum.JACKSON, mapper.readValue("[" + quote("JACKSON") + "]", TestEnum.class));
}
public void testUnwrappedEnumException() throws Exception {
final ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS);
try {
Object v = mapper.readValue("[" + quote("JACKSON") + "]",
TestEnum.class);
fail("Exception was not thrown on deserializing a single array element of type enum; instead got: "+v);
} catch (JsonMappingException exp) {
//exception as thrown correctly
verifyException(exp, "Can not deserialize");
}
}
// [databind#149]: 'stringified' indexes for enums
public void testIndexAsString() throws Exception
{
// first, regular index ought to work fine
TestEnum en = MAPPER.readValue("2", TestEnum.class);
assertSame(TestEnum.values()[2], en);
// but also with quoted Strings
en = MAPPER.readValue(quote("1"), TestEnum.class);
assertSame(TestEnum.values()[1], en);
}
public void testEnumWithJsonPropertyRename() throws Exception
{
String json = MAPPER.writeValueAsString(new EnumWithPropertyAnno[] {
EnumWithPropertyAnno.B, EnumWithPropertyAnno.A
});
assertEquals("[\"b\",\"a\"]", json);
// and while not really proper place, let's also verify deser while we're at it
EnumWithPropertyAnno[] result = MAPPER.readValue(json, EnumWithPropertyAnno[].class);
assertNotNull(result);
assertEquals(2, result.length);
assertSame(EnumWithPropertyAnno.B, result[0]);
assertSame(EnumWithPropertyAnno.A, result[1]);
}
// [databind#1161], unable to switch READ_ENUMS_USING_TO_STRING
public void testDeserWithToString1161() throws Exception
{
Enum1161 result = MAPPER.readerFor(Enum1161.class)
.readValue(quote("A"));
assertSame(Enum1161.A, result);
result = MAPPER.readerFor(Enum1161.class)
.with(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
.readValue(quote("a"));
assertSame(Enum1161.A, result);
// and once again, going back to defaults
result = MAPPER.readerFor(Enum1161.class)
.without(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
.readValue(quote("A"));
assertSame(Enum1161.A, result);
}
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 testEnumWithDefaultAnnotationUsingIndexInBound1() throws Exception {
final ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);
EnumWithDefaultAnno myEnum = mapper.readValue("1", EnumWithDefaultAnno.class);
assertSame(EnumWithDefaultAnno.B, myEnum);
}
public void testEnumWithDefaultAnnotationUsingIndexInBound2() throws Exception {
final ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);
EnumWithDefaultAnno myEnum = mapper.readValue("2", EnumWithDefaultAnno.class);
assertSame(EnumWithDefaultAnno.OTHER, myEnum);
}
public void testEnumWithDefaultAnnotationUsingIndexSameAsLength() throws Exception {
final ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);
EnumWithDefaultAnno myEnum = mapper.readValue("3", EnumWithDefaultAnno.class);
assertSame(EnumWithDefaultAnno.OTHER, myEnum);
}
public void testEnumWithDefaultAnnotationUsingIndexOutOfBound() throws Exception {
final ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);
EnumWithDefaultAnno myEnum = mapper.readValue("4", 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);
}
public void testExceptionFromCustomEnumKeyDeserializer() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new EnumModule());
try {
objectMapper.readValue("{\"TWO\": \"dumpling\"}",
new TypeReference<Map<AnEnum, String>>() {});
fail("No exception");
} catch (IOException e) {
assertTrue(e.getMessage().contains("Undefined AnEnum"));
}
}
}