Skip to content

Commit

Permalink
Bit more work on #1313 to also support @JsonFormat way of per-prope…
Browse files Browse the repository at this point in the history
…rty allowance (or not) of case-insensitivity
  • Loading branch information
cowtowncoder committed Feb 23, 2017
1 parent 15d4fe2 commit 5285e4a
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 95 deletions.
4 changes: 4 additions & 0 deletions release-notes/CREDITS
Expand Up @@ -610,3 +610,7 @@ Fabrizio Cucci (fabriziocucci@github)
Emiliano Clariá (emilianogc@github)
* Contributed #1434: Explicitly pass null on invoke calls with no arguments
(2.9.0)

Ana Eliza Barbosa (AnaEliza@github)
* Contributed #1520: Case insensitive enum deserialization feature.
(2.9.0)
2 changes: 2 additions & 0 deletions release-notes/VERSION
Expand Up @@ -52,6 +52,8 @@ Project: jackson-databind
(suggested by PawelJagus@github)
#1454: Support `@JsonFormat.lenient` for `java.util.Date`, `java.util.Calendar`
#1474: Replace use of `Class.newInstance()` (deprecated in Java 9) with call via Constructor
#1520: Case insensitive enum deserialization feature.
(contributed by Ana-Eliza B)

2.8.8 (not yet released)

Expand Down
Expand Up @@ -92,28 +92,7 @@ public enum DeserializationFeature implements ConfigFeature
* {@link java.util.List}s.
*/
USE_JAVA_ARRAY_FOR_JSON_ARRAY(false),

/**
* Feature that determines standard deserialization mechanism used for
* Enum values: if enabled, Enums are assumed to have been serialized using
* return value of <code>Enum.toString()</code>;
* if disabled, return value of <code>Enum.name()</code> is assumed to have been used.
*<p>
* Note: this feature should usually have same value
* as {@link SerializationFeature#WRITE_ENUMS_USING_TO_STRING}.
*<p>
* Feature is disabled by default.
*/
READ_ENUMS_USING_TO_STRING(false),

/**
* Feature that determines if Enum deserialization should be case sensitive or not.
* If enabled, Enum deserialization will ignore case.
* <p>
* Feature is disabled by default.
*/
READ_ENUMS_IGNORING_CASE(false),


/*
/******************************************************
* Error handling features
Expand Down Expand Up @@ -373,6 +352,31 @@ public enum DeserializationFeature implements ConfigFeature
*/
ACCEPT_FLOAT_AS_INT(true),

/**
* Feature that determines standard deserialization mechanism used for
* Enum values: if enabled, Enums are assumed to have been serialized using
* return value of <code>Enum.toString()</code>;
* if disabled, return value of <code>Enum.name()</code> is assumed to have been used.
*<p>
* Note: this feature should usually have same value
* as {@link SerializationFeature#WRITE_ENUMS_USING_TO_STRING}.
*<p>
* Feature is disabled by default.
*/
READ_ENUMS_USING_TO_STRING(false),

/**
* Feature that determines if Enum deserialization should be case sensitive or not.
* If enabled, Enum deserialization will ignore case, that is, case of incoming String
* value and enum id (dependant on other settings, either `name()`, `toString()`, or
* explicit override) do not need to match.
* <p>
* Feature is disabled by default.
*
* @since 2.9
*/
READ_ENUMS_IGNORING_CASE(false),

/**
* Feature that allows unknown Enum values to be parsed as null values.
* If disabled, unknown Enum values will throw exceptions.
Expand Down
Expand Up @@ -2,9 +2,13 @@

import java.io.IOException;

import com.fasterxml.jackson.annotation.JsonFormat;

import com.fasterxml.jackson.core.*;

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.deser.ValueInstantiator;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
Expand All @@ -19,6 +23,7 @@
@JacksonStdImpl // was missing until 2.6
public class EnumDeserializer
extends StdScalarDeserializer<Object>
implements ContextualDeserializer
{
private static final long serialVersionUID = 1L;

Expand All @@ -41,13 +46,28 @@ public class EnumDeserializer
* @since 2.7.3
*/
protected CompactStringObjectMap _lookupByToString;


protected final Boolean _caseInsensitive;

public EnumDeserializer(EnumResolver byNameResolver)
{
super(byNameResolver.getEnumClass());
_lookupByName = byNameResolver.constructLookup();
_enumsByIndex = byNameResolver.getRawEnums();
_enumDefaultValue = byNameResolver.getDefaultValue();
_caseInsensitive = false;
}

/**
* @since 2.9
*/
protected EnumDeserializer(EnumDeserializer base, Boolean caseInsensitive)
{
super(base);
_lookupByName = base._lookupByName;
_enumsByIndex = base._enumsByIndex;
_enumDefaultValue = base._enumDefaultValue;
_caseInsensitive = caseInsensitive;
}

/**
Expand Down Expand Up @@ -98,6 +118,25 @@ public static JsonDeserializer<?> deserializerForNoArgsCreator(DeserializationCo
return new FactoryBasedEnumDeserializer(enumClass, factory);
}

/**
* @since 2.9
*/
public EnumDeserializer withResolved(Boolean caseInsensitive) {
if (_caseInsensitive == caseInsensitive) {
return this;
}
return new EnumDeserializer(this, caseInsensitive);
}

@Override // since 2.9
public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
BeanProperty property) throws JsonMappingException
{
Boolean caseInsensitive = findFormatFeature(ctxt, property, handledType(),
JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
return withResolved(caseInsensitive);
}

/*
/**********************************************************
/* Default JsonDeserializer implementation
Expand Down Expand Up @@ -167,24 +206,28 @@ private final Object _deserializeAltString(JsonParser p, DeserializationContext
if (ctxt.isEnabled(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)) {
return null;
}
} else if (ctxt.isEnabled(DeserializationFeature.READ_ENUMS_IGNORING_CASE)) {
} else {
// [databind#1313]: Case insensitive enum deserialization
for (String key : lookup.keys()) {
if (key.equalsIgnoreCase(name)) {
return lookup.find(key);
if ((_caseInsensitive == Boolean.TRUE) ||
((_caseInsensitive == null) &&
ctxt.isEnabled(DeserializationFeature.READ_ENUMS_IGNORING_CASE))) {
for (String key : lookup.keys()) {
if (key.equalsIgnoreCase(name)) {
return lookup.find(key);
}
}
}
} else if (!ctxt.isEnabled(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS)) {
// [databind#149]: Allow use of 'String' indexes as well -- unless prohibited (as per above)
char c = name.charAt(0);
if (c >= '0' && c <= '9') {
try {
int index = Integer.parseInt(name);
if (index >= 0 && index < _enumsByIndex.length) {
return _enumsByIndex[index];
} else if (!ctxt.isEnabled(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS)) {
// [databind#149]: Allow use of 'String' indexes as well -- unless prohibited (as per above)
char c = name.charAt(0);
if (c >= '0' && c <= '9') {
try {
int index = Integer.parseInt(name);
if (index >= 0 && index < _enumsByIndex.length) {
return _enumsByIndex[index];
}
} catch (NumberFormatException e) {
// fine, ignore, was not an integer
}
} catch (NumberFormatException e) {
// fine, ignore, was not an integer
}
}
}
Expand Down
Expand Up @@ -63,7 +63,7 @@ public EnumSetDeserializer(JavaType enumType, JsonDeserializer<?> deser)
@SuppressWarnings("unchecked" )
protected EnumSetDeserializer(EnumSetDeserializer base,
JsonDeserializer<?> deser, Boolean unwrapSingle) {
super(EnumSet.class);
super(base);
_enumType = base._enumType;
_enumClass = base._enumClass;
_enumDeserializer = (JsonDeserializer<Enum<?>>) deser;
Expand Down
@@ -0,0 +1,128 @@
package com.fasterxml.jackson.databind.deser.jdk;

import java.io.IOException;
import java.util.EnumSet;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.BaseMapTest;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;

public class EnumAltIdTest extends BaseMapTest
{
// [databind#1313]

enum TestEnum { JACKSON, RULES, OK; }
protected enum LowerCaseEnum {
A, B, C;
private LowerCaseEnum() { }
@Override
public String toString() { return name().toLowerCase(); }
}

protected static class EnumBean {
@JsonFormat(with={ JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES })
public TestEnum value;
}

protected static class StrictCaseBean {
@JsonFormat(without={ JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES })
public TestEnum value;
}

/*
/**********************************************************
/* Test methods, basic
/**********************************************************
*/

protected final ObjectMapper MAPPER = new ObjectMapper();

protected final ObjectReader READER_DEFAULT = MAPPER.reader();
protected final ObjectReader READER_IGNORE_CASE = MAPPER
.reader(DeserializationFeature.READ_ENUMS_IGNORING_CASE);

// Tests for [databind#1313], case-insensitive

public void testFailWhenCaseSensitiveAndNameIsNotUpperCase() throws IOException {
try {
READER_DEFAULT.forType(TestEnum.class).readValue("\"Jackson\"");
fail("InvalidFormatException expected");
} catch (InvalidFormatException e) {
verifyException(e, "value not one of declared Enum instance names: [JACKSON, OK, RULES]");
}
}

public void testFailWhenCaseSensitiveAndToStringIsUpperCase() throws IOException {
ObjectReader r = READER_DEFAULT.forType(LowerCaseEnum.class)
.with(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
try {
r.readValue("\"A\"");
fail("InvalidFormatException expected");
} catch (InvalidFormatException e) {
verifyException(e, "value not one of declared Enum instance names: [a, b, c]");
}
}

public void testEnumDesIgnoringCaseWithLowerCaseContent() throws IOException {
assertEquals(TestEnum.JACKSON,
READER_IGNORE_CASE.forType(TestEnum.class).readValue(quote("jackson")));
}

public void testEnumDesIgnoringCaseWithUpperCaseToString() throws IOException {
ObjectReader r = MAPPER.readerFor(LowerCaseEnum.class)
.with(DeserializationFeature.READ_ENUMS_USING_TO_STRING,
DeserializationFeature.READ_ENUMS_IGNORING_CASE);
assertEquals(LowerCaseEnum.A, r.readValue("\"A\""));
}

/*
/**********************************************************
/* Test methods, containers
/**********************************************************
*/

public void testIgnoreCaseInEnumList() throws Exception {
TestEnum[] enums = READER_IGNORE_CASE.forType(TestEnum[].class)
.readValue("[\"jackson\", \"rules\"]");

assertEquals(2, enums.length);
assertEquals(TestEnum.JACKSON, enums[0]);
assertEquals(TestEnum.RULES, enums[1]);
}

public void testIgnoreCaseInEnumSet() throws IOException {
ObjectReader r = READER_IGNORE_CASE.forType(new TypeReference<EnumSet<TestEnum>>() { });
EnumSet<TestEnum> set = r.readValue("[\"jackson\"]");
assertEquals(1, set.size());
assertTrue(set.contains(TestEnum.JACKSON));
}

/*
/**********************************************************
/* Test methods, property overrides
/**********************************************************
*/

public void testIgnoreCaseViaFormat() throws Exception
{
final String JSON = aposToQuotes("{'value':'ok'}");

// should be able to allow on per-case basis:
EnumBean pojo = READER_DEFAULT.forType(EnumBean.class)
.readValue(JSON);
assertEquals(TestEnum.OK, pojo.value);

// including disabling acceptance
try {
READER_DEFAULT.forType(StrictCaseBean.class)
.readValue(JSON);
fail("Should not pass");
} catch (InvalidFormatException e) {
verifyException(e, "value not one of declared Enum instance names: [JACKSON, OK, RULES]");
}
}
}
@@ -1,4 +1,4 @@
package com.fasterxml.jackson.databind.deser;
package com.fasterxml.jackson.databind.deser.jdk;

import java.io.IOException;

Expand Down

0 comments on commit 5285e4a

Please sign in to comment.