Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1099,7 +1099,8 @@ public ValueDeserializer<?> createEnumDeserializer(DeserializationContext ctxt,
config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS),
constructEnumNamingStrategyResolver(config, beanDescRef.getClassInfo()),
// since 2.16
EnumResolver.constructUsingToString(config, beanDescRef.getClassInfo())
EnumResolver.constructUsingToString(config, beanDescRef.getClassInfo()),
constructEnumResolverNumberShape(enumClass, config, beanDescRef.get())
);
}
}
Expand Down Expand Up @@ -1728,6 +1729,20 @@ protected EnumResolver constructEnumNamingStrategyResolver(DeserializationConfig
: EnumResolver.constructUsingEnumNamingStrategy(config, enumClass, enumNamingStrategy);
}

protected EnumResolver constructEnumResolverNumberShape(Class<?> enumClass,
DeserializationConfig config, BeanDescription beanDesc)
{
AnnotatedMember jvAcc = beanDesc.findJsonValueAccessor();
if (jvAcc != null) {
if (config.canOverrideAccessModifiers()) {
ClassUtil.checkAndFixAccess(jvAcc.getMember(),
config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
}
return EnumResolver.constructUsingNumberShape(config, beanDesc.getClassInfo(), jvAcc);
}
return null;
}

protected boolean _hasCreatorAnnotation(MapperConfig<?> config,
Annotated ann) {
AnnotationIntrospector intr = config.getAnnotationIntrospector();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ public class EnumDeserializer
*/
protected final CompactStringObjectMap _lookupByEnumNaming;

/**
* We may also have integer-type of representation for Enum's, along with `@JsonValue`.
*
*/
protected final CompactStringObjectMap _lookupByShapeNumberInt;

/**
* Flag to check if FormatShape of int number type would be used to deserialize
*
*/
protected final boolean _isShapeNumberInt;

/**
<<<<<<< HEAD:src/main/java/tools/jackson/databind/deser/jdk/EnumDeserializer.java
=======
Expand Down Expand Up @@ -92,8 +104,10 @@ public EnumDeserializer(EnumResolver byNameResolver, boolean caseInsensitive,
_enumDefaultValue = byNameResolver.getDefaultValue();
_caseInsensitive = caseInsensitive;
_isFromIntValue = byNameResolver.isFromIntValue();
_isShapeNumberInt = false;
_lookupByEnumNaming = byEnumNamingResolver == null ? null : byEnumNamingResolver.constructLookup();
_lookupByToString = null;
_lookupByShapeNumberInt = null;
}

/**
Expand All @@ -102,6 +116,12 @@ public EnumDeserializer(EnumResolver byNameResolver, boolean caseInsensitive,
*/
public EnumDeserializer(EnumResolver byNameResolver, boolean caseInsensitive,
EnumResolver byEnumNamingResolver, EnumResolver toStringResolver)
{
this(byNameResolver, caseInsensitive, byEnumNamingResolver, toStringResolver, null);
}

public EnumDeserializer(EnumResolver byNameResolver, boolean caseInsensitive,
EnumResolver byEnumNamingResolver, EnumResolver toStringResolver, EnumResolver shapeNumberResolver)
{
super(byNameResolver.getEnumClass());
_lookupByName = byNameResolver.constructLookup();
Expand All @@ -110,8 +130,10 @@ public EnumDeserializer(EnumResolver byNameResolver, boolean caseInsensitive,
_enumDefaultValue = byNameResolver.getDefaultValue();
_caseInsensitive = caseInsensitive;
_isFromIntValue = byNameResolver.isFromIntValue();
_isShapeNumberInt = shapeNumberResolver != null;
_lookupByEnumNaming = byEnumNamingResolver == null ? null : byEnumNamingResolver.constructLookup();
_lookupByToString = toStringResolver == null ? null : toStringResolver.constructLookup();
_lookupByShapeNumberInt = shapeNumberResolver == null ? null : shapeNumberResolver.constructLookup();
}

protected EnumDeserializer(EnumDeserializer base, boolean caseInsensitive,
Expand All @@ -124,10 +146,13 @@ protected EnumDeserializer(EnumDeserializer base, boolean caseInsensitive,
_enumDefaultValue = base._enumDefaultValue;
_caseInsensitive = caseInsensitive;
_isFromIntValue = base._isFromIntValue;
_isShapeNumberInt = base._isShapeNumberInt;
_useDefaultValueForUnknownEnum = useDefaultValueForUnknownEnum;
_useNullForUnknownEnum = useNullForUnknownEnum;
_lookupByEnumNaming = base._lookupByEnumNaming;
_lookupByToString = base._lookupByToString;
_lookupByShapeNumberInt = base._lookupByShapeNumberInt;

}

/**
Expand Down Expand Up @@ -229,6 +254,10 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt)
// 26-Sep-2021, tatu: [databind#1850] Special case where we get "true" integer
// enumeration and should avoid use of {@code Enum.index()}
if (_isFromIntValue) {
// [databind#3580] Enum (de)serialization in conjunction with JsonFormat.Shape.NUMBER_INT
if (_isShapeNumberInt) {
return _fromInteger(p, ctxt, p.getIntValue());
}
// ... whether to rely on "getText()" returning String, or get number, convert?
// For now assume all format backends can produce String:
return _fromString(p, ctxt, p.getString());
Expand Down Expand Up @@ -273,7 +302,7 @@ private CompactStringObjectMap _resolveCurrentLookup(DeserializationContext ctxt
}

protected Object _fromInteger(JsonParser p, DeserializationContext ctxt,
int index)
int intValue)
throws JacksonException
{
final CoercionAction act = ctxt.findCoercionAction(logicalType(), handledType(),
Expand All @@ -282,13 +311,13 @@ protected Object _fromInteger(JsonParser p, DeserializationContext ctxt,
// First, check legacy setting for slightly different message
if (act == CoercionAction.Fail) {
if (ctxt.isEnabled(EnumFeature.FAIL_ON_NUMBERS_FOR_ENUMS)) {
return ctxt.handleWeirdNumberValue(_enumClass(), index,
return ctxt.handleWeirdNumberValue(_enumClass(), intValue,
"not allowed to deserialize Enum value out of number: disable DeserializationConfig.EnumFeature.FAIL_ON_NUMBERS_FOR_ENUMS to allow"
);
}
// otherwise this will force failure with new setting
_checkCoercionFail(ctxt, act, handledType(), index,
"Integer value ("+index+")");
_checkCoercionFail(ctxt, act, handledType(), intValue,
"Integer value ("+intValue+")");
}
switch (act) {
case AsNull:
Expand All @@ -298,14 +327,26 @@ protected Object _fromInteger(JsonParser p, DeserializationContext ctxt,
case TryConvert:
default:
}
if (index >= 0 && index < _enumsByIndex.length) {
return _enumsByIndex[index];

// [databind#3580] Enum (de)serialization in conjunction with JsonFormat.Shape.NUMBER_INT
if (_isShapeNumberInt) {
Object numberShape = _lookupByShapeNumberInt.find(String.valueOf(intValue));
if (numberShape != null) {
return numberShape;
} else {
return ctxt.handleWeirdNumberValue(_enumClass(), intValue,
"Number Int value is not one of expected values",
_lookupByShapeNumberInt.toString());
}
}
if (intValue >= 0 && intValue < _enumsByIndex.length) {
return _enumsByIndex[intValue];
}
if (useDefaultValueForUnknownEnum(ctxt)) {
return _enumDefaultValue;
}
if (!useNullForUnknownEnum(ctxt)) {
return ctxt.handleWeirdNumberValue(_enumClass(), index,
return ctxt.handleWeirdNumberValue(_enumClass(), intValue,
"index value outside legal index range [0..%s]",
_enumsByIndex.length-1);
}
Expand Down
47 changes: 47 additions & 0 deletions src/main/java/tools/jackson/databind/util/EnumResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.*;

import com.fasterxml.jackson.annotation.JsonFormat;
import tools.jackson.databind.*;
import tools.jackson.databind.cfg.MapperConfig;
import tools.jackson.databind.introspect.AnnotatedClass;
Expand Down Expand Up @@ -271,6 +272,52 @@ public static EnumResolver constructUsingMethod(DeserializationConfig config,
);
}

/**
* Method used when ALL of conditions below are met
*<p>
* 1. actual String serialization is indicated using @JsonValue on a method in Enum class AND
* 2. Enum class is annotated with `@JsonFormat`
*
*/
public static EnumResolver constructUsingNumberShape(DeserializationConfig config, AnnotatedClass annotatedClass, AnnotatedMember accessor)
{
// prepare data
final boolean isIgnoreCase = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
final Class<?> enumCls0 = annotatedClass.getRawType();
final Class<Enum<?>> enumCls = _enumClass(enumCls0);
final Enum<?>[] enumConstants = _enumConstants(enumCls);
final Enum<?> defaultEnum = _enumDefault(config, annotatedClass, enumConstants);

// introspect
HashMap<String, Enum<?>> map = new HashMap<>();
final AnnotationIntrospector ai = config.getAnnotationIntrospector();
JsonFormat.Value format = ai.findFormat(config, annotatedClass);
if (format == null) {
return null;
}
if (format.getShape() != JsonFormat.Shape.NUMBER_INT) {
throw new IllegalArgumentException("Failed to access @JsonValue of Enum value ");
}
for (int i = enumConstants.length; --i >= 0; ) { // from last to first, so that in case of duplicate values, first wins
Enum<?> en = enumConstants[i];
try {
Object o = accessor.getValue(en);
if (o != null) {
map.put(o.toString(), en);
}
} catch (Exception e) {
throw new IllegalArgumentException("Failed to access @JsonValue of Enum value "+en+": "+e.getMessage());
}
}

// finally build
return new EnumResolver(enumCls, enumConstants, map, defaultEnum, isIgnoreCase,
// 26-Sep-2021, tatu: [databind#1850] Need to consider "from int" case
_isIntType(accessor.getRawType()),
true
);
}

@SuppressWarnings("unchecked")
protected static Class<Enum<?>> _enumClass(Class<?> cls) {
return (Class<Enum<?>>) cls;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package tools.jackson.databind.format;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonValue;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.exc.InvalidFormatException;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

// [databind#3580] Enum (de)serialization in conjunction with JsonFormat.Shape.NUMBER_INT
public class EnumNumberFormatShape3580Test
extends DatabindTestUtil
{
public static class Pojo3580 {
public PojoState3580 state;
public Pojo3580() {}
public Pojo3580(PojoState3580 state) {this.state = state;}
}

@JsonFormat(shape = JsonFormat.Shape.NUMBER_INT)
public enum PojoState3580 {
OFF(17),
ON(31),
UNKNOWN(99);

private int value;

PojoState3580(int value) { this.value = value; }

@JsonValue
public int value() {return this.value;}
}

@Test
public void testEnumNumberFormatShape3580()
throws Exception
{
ObjectMapper mapper = JsonMapper.builder().build();

// Serialize
assertEquals("{\"state\":17}", mapper.writeValueAsString(new Pojo3580(PojoState3580.OFF))); //
assertEquals("{\"state\":31}", mapper.writeValueAsString(new Pojo3580(PojoState3580.ON))); //
assertEquals("{\"state\":99}", mapper.writeValueAsString(new Pojo3580(PojoState3580.UNKNOWN))); //

// Pass Deserialize
assertEquals(PojoState3580.OFF, mapper.readValue("{\"state\":17}", Pojo3580.class).state); // Pojo[state=OFF]
assertEquals(PojoState3580.ON, mapper.readValue("{\"state\":31}", Pojo3580.class).state); // Pojo[state=OFF]
assertEquals(PojoState3580.UNKNOWN, mapper.readValue("{\"state\":99}", Pojo3580.class).state); // Pojo[state=OFF]

// Fail : Try to use ordinal number
assertThrows(InvalidFormatException.class, () -> mapper.readValue("{\"state\":0}", Pojo3580.class));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package tools.jackson.databind.records;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonValue;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.exc.InvalidFormatException;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

// [databind#3580] Enum (de)serialization in conjunction with JsonFormat.Shape.NUMBER_INT
public class EnumNumberFormatShapeRecord3580Test
extends DatabindTestUtil
{
public record Record3580(@JsonFormat(shape = JsonFormat.Shape.NUMBER) RecordState3580 state) {}

public enum RecordState3580 {
OFF(17),
ON(31),
UNKNOWN(99);

private int value;

RecordState3580(int value) { this.value = value; }

@JsonValue
public int value() {return this.value;}
}

@Test
public void testEnumNumberFormatShapeRecord3580()
throws Exception
{
ObjectMapper mapper = JsonMapper.builder().build();

// Serialize
assertEquals("{\"state\":17}", mapper.writeValueAsString(new Record3580(RecordState3580.OFF))); //
assertEquals("{\"state\":31}", mapper.writeValueAsString(new Record3580(RecordState3580.ON))); //
assertEquals("{\"state\":99}", mapper.writeValueAsString(new Record3580(RecordState3580.UNKNOWN))); //

// Pass Deserialize
assertEquals(RecordState3580.OFF, mapper.readValue("{\"state\":17}", Record3580.class).state); // Pojo[state=OFF]
assertEquals(RecordState3580.ON, mapper.readValue("{\"state\":31}", Record3580.class).state); // Pojo[state=OFF]
assertEquals(RecordState3580.UNKNOWN, mapper.readValue("{\"state\":99}", Record3580.class).state); // Pojo[state=OFF]

// Fail : Try to use ordinal number
assertThrows(InvalidFormatException.class, () -> mapper.readValue("{\"state\":0}", Record3580.class));
}
}