Skip to content
Closed
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 @@ -1100,7 +1100,8 @@ public JsonDeserializer<?> createEnumDeserializer(DeserializationContext ctxt,
config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS),
constructEnumNamingStrategyResolver(config, beanDesc.getClassInfo()),
// since 2.16
EnumResolver.constructUsingToString(config, beanDesc.getClassInfo())
EnumResolver.constructUsingToString(config, beanDesc.getClassInfo()),
constructEnumResolverNumberShape(enumClass, config, beanDesc)
);
}
}
Expand Down Expand Up @@ -1815,6 +1816,21 @@ protected EnumResolver constructEnumResolver(Class<?> enumClass,
return EnumResolver.constructFor(config, beanDesc.getClassInfo());
}


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

/**
* Factory method used to resolve an instance of {@link CompactStringObjectMap}
* with {@link EnumNamingStrategy} applied for the target class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,22 @@ public class EnumDeserializer
*/
protected volatile CompactStringObjectMap _lookupByToString;

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

protected final Boolean _caseInsensitive;

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

private Boolean _useDefaultValueForUnknownEnum;
private Boolean _useNullForUnknownEnum;

Expand All @@ -71,7 +85,7 @@ public class EnumDeserializer
* Marker flag that indicates whether the Enum class has {@code @JsonValue}
* annotated accessor (or equivalent), used to populate {@link #_lookupByName}.
*
* @since 2.20
* @since 2.21.0
*/
protected final boolean _hasAsValueAnnotation;

Expand Down Expand Up @@ -109,15 +123,28 @@ 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;
}

/**
* @since 2.16
* @deprecated since 2.21.0
*/
@Deprecated
public EnumDeserializer(EnumResolver byNameResolver, boolean caseInsensitive,
EnumResolver byEnumNamingResolver, EnumResolver toStringResolver)
{
this(byNameResolver, caseInsensitive, byEnumNamingResolver, toStringResolver, null);
}

/**
* @since 2.21.0
*/
public EnumDeserializer(EnumResolver byNameResolver, boolean caseInsensitive,
EnumResolver byEnumNamingResolver, EnumResolver toStringResolver, EnumResolver shapeNumberResolver)
{
super(byNameResolver.getEnumClass());
_lookupByName = byNameResolver.constructLookup();
Expand All @@ -126,8 +153,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();
}

/**
Expand All @@ -143,10 +172,12 @@ protected EnumDeserializer(EnumDeserializer base, Boolean caseInsensitive,
_enumDefaultValue = base._enumDefaultValue;
_caseInsensitive = Boolean.TRUE.equals(caseInsensitive);
_isFromIntValue = base._isFromIntValue;
_isShapeNumberInt = base._isShapeNumberInt;
_useDefaultValueForUnknownEnum = useDefaultValueForUnknownEnum;
_useNullForUnknownEnum = useNullForUnknownEnum;
_lookupByEnumNaming = base._lookupByEnumNaming;
_lookupByToString = base._lookupByToString;
_lookupByShapeNumberInt = base._lookupByShapeNumberInt;
}

/**
Expand Down Expand Up @@ -238,7 +269,7 @@ public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
JsonFormat.Feature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)).orElse(_useDefaultValueForUnknownEnum);
Boolean useNullForUnknownEnum = Optional.ofNullable(findFormatFeature(ctxt, property, handledType(),
JsonFormat.Feature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)).orElse(_useNullForUnknownEnum);

return withResolved(caseInsensitive, useDefaultValueForUnknownEnum, useNullForUnknownEnum);
}

Expand Down Expand Up @@ -280,6 +311,10 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
// 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.getText());
Expand Down Expand Up @@ -327,7 +362,7 @@ private CompactStringObjectMap _resolveCurrentLookup(DeserializationContext ctxt
}

protected Object _fromInteger(JsonParser p, DeserializationContext ctxt,
int index)
int intValue)
throws IOException
{
final CoercionAction act = ctxt.findCoercionAction(logicalType(), handledType(),
Expand All @@ -336,13 +371,13 @@ protected Object _fromInteger(JsonParser p, DeserializationContext ctxt,
// First, check legacy setting for slightly different message
if (act == CoercionAction.Fail) {
if (ctxt.isEnabled(DeserializationFeature.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.DeserializationFeature.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 @@ -352,14 +387,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 Expand Up @@ -470,7 +517,7 @@ protected Class<?> _enumClass() {

/**
* Since 2.16, {@link #_lookupByToString} it is passed via
* {@link #EnumDeserializer(EnumResolver, boolean, EnumResolver, EnumResolver)}, so there is no need for lazy
* {@link #EnumDeserializer(EnumResolver, boolean, EnumResolver, EnumResolver, EnumResolver)}, so there is no need for lazy
* initialization. But kept for backward-compatilibility reasons. In case {@link #_lookupByToString} is null.
*
* @deprecated Since 2.16
Expand Down
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 com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.EnumNamingStrategy;
Expand Down Expand Up @@ -302,6 +303,53 @@ 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`
*
* @since 2.21.0
*/
public static EnumResolver constructUsingNumberShape(DeserializationConfig config, AnnotatedClass annotatedClass, AnnotatedMember accessor)
{
// prepare data
final AnnotationIntrospector ai = config.getAnnotationIntrospector();
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(enumCls0);
HashMap<String, Enum<?>> map = new HashMap<>();

JsonFormat.Value format = ai.findFormat(annotatedClass);
if (format == null) {
return null;
}
if (format.getShape() != JsonFormat.Shape.NUMBER_INT) {
throw new IllegalArgumentException("Failed to access @JsonValue of Enum value ");
}

// from last to first, so that in case of duplicate values, first wins
for (int i = enumConstants.length; --i >= 0; ) {
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());
}
}
return new EnumResolver(enumCls, enumConstants, map,
_enumDefault(ai, annotatedClass, enumConstants),
isIgnoreCase,
// 26-Sep-2021, tatu: [databind#1850] Need to consider "from int" case
_isIntType(accessor.getRawType()),
true
);
}

public CompactStringObjectMap constructLookup() {
return CompactStringObjectMap.construct(_enumsById);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.fasterxml.jackson.databind.records;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.fasterxml.jackson.databind.format;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.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));
}
}