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")); } } }