Skip to content
2 changes: 2 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Versions: 3.x (for earlier see VERSION-2.x)
with Jackson 3
(reported by Andy W)
(fix contributed by Fouad A)
#5432: Jackson 3 ignores `@JsonProperty` when Enum value is used as Map key
(reported by @jochenberger)

3.0.2 (07-Nov-2025)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -939,17 +939,17 @@ public Boolean hasAnyGetter(MapperConfig<?> config, Annotated ann) {
}

/**
* Finds the explicitly defined name of the given set of {@code Enum} values, if any.
* Finds the explicitly defined names, if any, of the given set of {@code Enum} values.
* The method overwrites entries in the incoming {@code names} array with the explicit
* names found, if any, leaving other entries unmodified.
*
* @param config the mapper configuration to use
* @param annotatedClass the annotated class for which to find the explicit names
* @param enumValues the set of {@code Enum} values to find the explicit names for
* @param names the matching declared names of enumeration values (with indexes matching
* {@code enumValues} entries)
* {@code enumValues} entries); modified as necessary
*
* @return an array of names to use (possibly {@code names} passed as argument)
* @return an array of names to use (usually {@code names} passed as argument)
*
* @since 2.16
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ public interface EnumNamingStrategy {
* Translates the given <code>enumName</code> into an external property name according to
* the implementation of this {@link EnumNamingStrategy}.
*
* @param enumName the name of the enum value to translate
* @param config the mapper configuration
* @param cls the Enum class
* @param enumName the name of the enum value to translate
*
* @return the external property name that corresponds to the given <code>enumName</code>
* according to the implementation of this {@link EnumNamingStrategy}.
Expand Down
1 change: 0 additions & 1 deletion src/main/java/tools/jackson/databind/cfg/EnumFeature.java
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ public enum EnumFeature implements DatatypeFeature
*/
WRITE_ENUMS_TO_LOWERCASE(false),


/**
* Feature that determines standard serialization mechanism used for
* Enum values: if enabled, return value of <code>Enum.toString()</code>
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/tools/jackson/databind/cfg/MapperConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,15 @@ public final boolean shouldSortPropertiesAlphabetically() {
*
* @param src Text to represent
*
* @return Optimized text object constructed
* @return Optimized text object constructed, if {@code src} not {@code null};
* {@code null} otherwise
*/
public SerializableString compileString(String src) {
// 20-Jan-2014, tatu: For now we will just construct it directly but in distant
// future might want to allow overriding somehow?
if (src == null) {
return null;
}
return new SerializedString(src);
}

Expand Down
63 changes: 21 additions & 42 deletions src/main/java/tools/jackson/databind/ser/jdk/JDKKeySerializers.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
import tools.jackson.databind.ser.impl.PropertySerializerMap;
import tools.jackson.databind.ser.std.StdSerializer;
import tools.jackson.databind.util.ClassUtil;
import tools.jackson.databind.util.EnumValues;
import tools.jackson.databind.util.EnumDefinition;
import tools.jackson.databind.util.EnumValuesToWrite;

public abstract class JDKKeySerializers
{
Expand Down Expand Up @@ -78,9 +79,8 @@ public static ValueSerializer<Object> getStdKeySerializer(SerializationConfig co

/**
* Method called if no specified key serializer was located; will return a
* "default" key serializer initialized by {@link EnumKeySerializer#construct(Class, EnumValues, EnumValues)}
* "default" key serializer except with special handling for {@code Enum}s
*/
@SuppressWarnings("unchecked")
public static ValueSerializer<Object> getFallbackKeySerializer(SerializationConfig config,
Class<?> rawKeyType, AnnotatedClass annotatedClass)
{
Expand All @@ -98,8 +98,7 @@ public static ValueSerializer<Object> getFallbackKeySerializer(SerializationConf
// for subtypes.
if (ClassUtil.isEnumType(rawKeyType)) {
return EnumKeySerializer.construct(rawKeyType,
EnumValues.constructFromName(config, annotatedClass),
EnumSerializer.constructEnumNamingStrategyValues(config, (Class<Enum<?>>) rawKeyType, annotatedClass));
EnumDefinition.construct(config, annotatedClass).valuesToWrite(config));
}
}
// 19-Oct-2016, tatu: Used to just return DEFAULT_KEY_SERIALIZER but why not:
Expand Down Expand Up @@ -265,56 +264,36 @@ public void serialize(Object value, JsonGenerator g, SerializationContext provid
*/
public static class EnumKeySerializer extends StdSerializer<Object>
{
protected final EnumValues _values;
protected final EnumValuesToWrite _valuesToWrite;

/**
* Map with key as converted property class defined implementation of {@link EnumNamingStrategy}
* and with value as Enum names collected using <code>Enum.name()</code>.
*/
protected final EnumValues _valuesByEnumNaming;

@Deprecated
protected EnumKeySerializer(Class<?> enumType, EnumValues values) {
this(enumType, values, null);
}

protected EnumKeySerializer(Class<?> enumType, EnumValues values, EnumValues valuesByEnumNaming) {
super(enumType);
_values = values;
_valuesByEnumNaming = valuesByEnumNaming;
}

public static EnumKeySerializer construct(Class<?> enumType,
EnumValues enumValues)
protected EnumKeySerializer(Class<?> enumType, EnumValuesToWrite valuesToWrite)
{
return new EnumKeySerializer(enumType, enumValues);
super(enumType);
_valuesToWrite = valuesToWrite;
}

public static EnumKeySerializer construct(Class<?> enumType,
EnumValues enumValues, EnumValues valuesByEnumNaming)
EnumValuesToWrite valuesToWrite)
{
return new EnumKeySerializer(enumType, enumValues, valuesByEnumNaming);
return new EnumKeySerializer(enumType, valuesToWrite);
}

@Override
public void serialize(Object value, JsonGenerator g, SerializationContext serializers)
public void serialize(Object value, JsonGenerator g, SerializationContext ctxt)
throws JacksonException
{
if (serializers.isEnabled(EnumFeature.WRITE_ENUMS_USING_TO_STRING)) {
g.writeName(value.toString());
return;
}
Enum<?> en = (Enum<?>) value;
if (_valuesByEnumNaming != null) {
g.writeName(_valuesByEnumNaming.serializedValueFor(en));
return;
}
// 14-Sep-2019, tatu: [databind#2129] Use this specific feature
if (serializers.isEnabled(EnumFeature.WRITE_ENUM_KEYS_USING_INDEX)) {
final Enum<?> en = (Enum<?>) value;

// 26-Nov-2025, tatu: Should probably start with "using index" setting
// (may change in 3.1?)
if (ctxt.isEnabled(EnumFeature.WRITE_ENUMS_USING_TO_STRING)) {
g.writeName(_valuesToWrite.fromToString(ctxt.getConfig(), en));
} else if (ctxt.isEnabled(EnumFeature.WRITE_ENUM_KEYS_USING_INDEX)) {
// 14-Sep-2019, tatu: [databind#2129] Use this specific feature
g.writeName(String.valueOf(en.ordinal()));
return;
} else {
g.writeName(_valuesToWrite.fromName(ctxt.getConfig(), en));
}
g.writeName(_values.serializedValueFor(en));
}
}
}
87 changes: 87 additions & 0 deletions src/main/java/tools/jackson/databind/util/EnumDefinition.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package tools.jackson.databind.util;

import java.util.Arrays;
import java.util.List;

import tools.jackson.databind.AnnotationIntrospector;
import tools.jackson.databind.EnumNamingStrategy;
import tools.jackson.databind.cfg.MapperConfig;
import tools.jackson.databind.introspect.AnnotatedClass;
import tools.jackson.databind.introspect.EnumNamingStrategyFactory;

/**
* Encapsulation of a {@link java.lang.Enum} type definition with its elements
* and explicitly annotated names for elements.
*
* @since 3.0.3
*/
public class EnumDefinition
{
private final AnnotatedClass _annotatedClass;
private final EnumNamingStrategy _enumNamingStrategy;
private final Enum<?>[] _enumConstants;
private final String[] _explicitNames;

private EnumDefinition(AnnotatedClass annotatedClass,
EnumNamingStrategy enumNamingStrategy,
Enum<?>[] enumConstants,
String[] explicitNames)
{
_annotatedClass = annotatedClass;
_enumNamingStrategy = enumNamingStrategy;
_enumConstants = enumConstants;
_explicitNames = explicitNames;
}

public static EnumDefinition construct(MapperConfig<?> config,
AnnotatedClass annotatedClass)
{
final Class<?> enumCls0 = annotatedClass.getRawType();
final Enum<?>[] enumConstants = _enumConstants(enumCls0);
String[] explicitNames = new String[enumConstants.length];

final AnnotationIntrospector ai = config.getAnnotationIntrospector();
if (ai != null) {
explicitNames = ai.findEnumValues(config, annotatedClass,
enumConstants, explicitNames);
}
Object namingDef = config.getAnnotationIntrospector().findEnumNamingStrategy(config, annotatedClass);
EnumNamingStrategy enumNamingStrategy = EnumNamingStrategyFactory.createEnumNamingStrategyInstance(
namingDef, config.canOverrideAccessModifiers(), config.getEnumNamingStrategy());

return new EnumDefinition(annotatedClass, enumNamingStrategy,
enumConstants, explicitNames);
}

public EnumValuesToWrite valuesToWrite(MapperConfig<?> config) {
return EnumValuesToWrite.construct(config, _annotatedClass,
_enumNamingStrategy,
_enumConstants, _explicitNames);
}

public int size() {
return _enumConstants.length;
}

@SuppressWarnings("unchecked")
public Class<Enum<?>> enumClass() {
Class<?> cls = _annotatedClass.getRawType();
return (Class<Enum<?>>) cls;
}

public Enum<?>[] enumConstants() {
return _enumConstants;
}

public List<String> explicitNames() {
return Arrays.asList(_explicitNames);
}

private static Enum<?>[] _enumConstants(Class<?> enumCls) {
final Enum<?>[] enumValues = ClassUtil.findEnumType(enumCls).getEnumConstants();
if (enumValues == null) {
throw new IllegalArgumentException("Internal error: no Enum constants for Class "+enumCls.getName());
}
return enumValues;
}
}
78 changes: 27 additions & 51 deletions src/main/java/tools/jackson/databind/util/EnumValues.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,66 +34,52 @@ private EnumValues(Class<Enum<?>> enumClass, SerializableString[] textual)
/**
* NOTE: do NOT call this if configuration may change, and choice between toString()
* and name() might change dynamically.
*
* @deprecated Since 3.1 call {@link #constructFromName} or {@link #constructFromToString}
* instead.
*/
@Deprecated // since 3.1
public static EnumValues construct(SerializationConfig config, AnnotatedClass enumClass) {
if (config.isEnabled(EnumFeature.WRITE_ENUMS_USING_TO_STRING)) {
return constructFromToString(config, enumClass);
}
return constructFromName(config, enumClass);
}

/**
* @since 2.16
*/
public static EnumValues constructFromName(MapperConfig<?> config, AnnotatedClass annotatedClass)
public static EnumValues constructFromName(MapperConfig<?> config,
AnnotatedClass annotatedClass)
{
// prepare data
final AnnotationIntrospector ai = config.getAnnotationIntrospector();
final boolean useLowerCase = config.isEnabled(EnumFeature.WRITE_ENUMS_TO_LOWERCASE);
final Class<?> enumCls0 = annotatedClass.getRawType();
final Class<Enum<?>> enumCls = _enumClass(enumCls0);
final Enum<?>[] enumConstants = _enumConstants(enumCls0);
final EnumDefinition def = EnumDefinition.construct(config, annotatedClass);
final Class<Enum<?>> enumCls = def.enumClass();
final Enum<?>[] enumConstants = def.enumConstants();

// introspect
String[] names = ai.findEnumValues(config, annotatedClass,
enumConstants, new String[enumConstants.length]);

// build
List<String> explicitNames = def.explicitNames();
SerializableString[] textual = new SerializableString[enumConstants.length];
final boolean useLowerCase = config.isEnabled(EnumFeature.WRITE_ENUMS_TO_LOWERCASE);
for (int i = 0, len = enumConstants.length; i < len; ++i) {
Enum<?> enumValue = enumConstants[i];
String name = _findNameToUse(names[i], enumValue.name(), useLowerCase);
String name = _findNameToUse(explicitNames.get(i), enumValue.name(), useLowerCase);
textual[enumValue.ordinal()] = config.compileString(name);
}
return construct(enumCls, textual);
}

/**
* @since 2.16
*/
public static EnumValues constructFromToString(MapperConfig<?> config, AnnotatedClass annotatedClass)
public static EnumValues constructFromToString(MapperConfig<?> config,
AnnotatedClass annotatedClass)
{
// prepare data
final AnnotationIntrospector ai = config.getAnnotationIntrospector();
final boolean useLowerCase = config.isEnabled(EnumFeature.WRITE_ENUMS_TO_LOWERCASE);
final Class<?> enumCls0 = annotatedClass.getRawType();
final Class<Enum<?>> enumCls = _enumClass(enumCls0);
final Enum<?>[] enumConstants = _enumConstants(enumCls0);

// introspect
String[] names = new String[enumConstants.length];
if (ai != null) {
ai.findEnumValues(config, annotatedClass, enumConstants, names);
}
final EnumDefinition def = EnumDefinition.construct(config, annotatedClass);
final Class<Enum<?>> enumCls = def.enumClass();
final Enum<?>[] enumConstants = def.enumConstants();

// build
List<String> explicitNames = def.explicitNames();
SerializableString[] textual = new SerializableString[enumConstants.length];
final boolean useLowerCase = config.isEnabled(EnumFeature.WRITE_ENUMS_TO_LOWERCASE);
for (int i = 0; i < enumConstants.length; i++) {
String enumToString = enumConstants[i].toString();
// 01-Feb-2024, tatu: [databind#4355] Nulls not great but... let's
// coerce into "" for backwards compatibility
enumToString = (enumToString == null) ? "" : enumToString;
String name = _findNameToUse(names[i], enumToString, useLowerCase);
String name = _findNameToUse(explicitNames.get(i), enumToString, useLowerCase);
textual[i] = config.compileString(name);
}
return construct(enumCls, textual);
Expand All @@ -104,31 +90,21 @@ public static EnumValues constructFromToString(MapperConfig<?> config, Annotated
* <p>
* The output {@link EnumValues} should contain values that are symmetric to
* {@link EnumResolver#constructUsingEnumNamingStrategy(DeserializationConfig, AnnotatedClass, EnumNamingStrategy)}.
*
* @since 2.16
*/
public static EnumValues constructUsingEnumNamingStrategy(MapperConfig<?> config,
AnnotatedClass annotatedClass,
EnumNamingStrategy namingStrategy)
{
// prepare data
final AnnotationIntrospector ai = config.getAnnotationIntrospector();
final boolean useLowerCase = config.isEnabled(EnumFeature.WRITE_ENUMS_TO_LOWERCASE);
final Class<?> enumCls0 = annotatedClass.getRawType();
final Class<Enum<?>> enumCls = _enumClass(enumCls0);
final Enum<?>[] enumConstants = _enumConstants(enumCls0);

// introspect
String[] names = new String[enumConstants.length];
if (ai != null) {
ai.findEnumValues(config, annotatedClass, enumConstants, names);
}
final EnumDefinition def = EnumDefinition.construct(config, annotatedClass);
final Class<Enum<?>> enumCls = def.enumClass();
final Enum<?>[] enumConstants = def.enumConstants();

// build
List<String> explicitNames = def.explicitNames();
SerializableString[] textual = new SerializableString[enumConstants.length];
final boolean useLowerCase = config.isEnabled(EnumFeature.WRITE_ENUMS_TO_LOWERCASE);
for (int i = 0, len = enumConstants.length; i < len; i++) {
Enum<?> enumValue = enumConstants[i];
String name = _findNameToUse(names[i], namingStrategy.convertEnumToExternalName(config,
String name = _findNameToUse(explicitNames.get(i), namingStrategy.convertEnumToExternalName(config,
annotatedClass, enumValue.name()), useLowerCase);
textual[i] = config.compileString(name);
}
Expand Down Expand Up @@ -215,7 +191,7 @@ public EnumMap<?,SerializableString> internalMap() {
EnumMap<?,SerializableString> result = _asMap;
if (result == null) {
// Alas, need to create it in a round-about way, due to typing constraints...
Map<Enum<?>,SerializableString> map = new LinkedHashMap<Enum<?>,SerializableString>();
Map<Enum<?>,SerializableString> map = new LinkedHashMap<>();
for (Enum<?> en : _values) {
map.put(en, _textual[en.ordinal()]);
}
Expand Down
Loading