diff --git a/src/main/java/tools/jackson/databind/ser/jdk/EnumSerializer.java b/src/main/java/tools/jackson/databind/ser/jdk/EnumSerializer.java index a44768c9c2..3a892924a0 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/EnumSerializer.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/EnumSerializer.java @@ -1,6 +1,7 @@ package tools.jackson.databind.ser.jdk; import java.util.LinkedHashSet; +import java.util.List; import java.util.Objects; import java.util.Set; @@ -11,12 +12,13 @@ import tools.jackson.databind.*; import tools.jackson.databind.annotation.JacksonStdImpl; import tools.jackson.databind.cfg.EnumFeature; -import tools.jackson.databind.introspect.AnnotatedClass; -import tools.jackson.databind.introspect.EnumNamingStrategyFactory; +import tools.jackson.databind.cfg.MapperConfig; import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; import tools.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor; import tools.jackson.databind.ser.std.StdScalarSerializer; +import tools.jackson.databind.util.EnumDefinition; import tools.jackson.databind.util.EnumValues; +import tools.jackson.databind.util.EnumValuesToWrite; /** * Standard serializer used for {@link java.lang.Enum} types. @@ -29,10 +31,9 @@ public class EnumSerializer extends StdScalarSerializer> { /** - * This map contains pre-resolved values (since there are ways to customize - * actual String constants to use) to use as serializations. + * Container for dynamically resolved serializations for the type. */ - protected final EnumValues _values; + protected final EnumValuesToWrite _enumValuesToWrite; /** * Flag that is set if we statically know serialization choice between @@ -40,51 +41,35 @@ public class EnumSerializer */ protected final Boolean _serializeAsIndex; - /** - * Map with key as converted property class defined implementation of {@link EnumNamingStrategy} - * and with value as Enum names collected using Enum.name(). - */ - protected final EnumValues _valuesByEnumNaming; - - /** - * Map that contains pre-resolved values for {@link Enum#toString} to use for serialization, - * while respecting {@link com.fasterxml.jackson.annotation.JsonProperty} - * and {@link tools.jackson.databind.cfg.EnumFeature#WRITE_ENUMS_TO_LOWERCASE}. - */ - protected final EnumValues _valuesByToString; - /* /********************************************************************** /* Life-cycle /********************************************************************** */ - public EnumSerializer(EnumValues v, Boolean serializeAsIndex, EnumValues valuesByEnumNaming, - EnumValues valuesByToString) + public EnumSerializer(EnumValuesToWrite enumValuesToWrite, Boolean serializeAsIndex) { - super(v.getEnumClass(), false); - _values = v; + super(enumValuesToWrite.enumClass(), false); + _enumValuesToWrite = enumValuesToWrite; _serializeAsIndex = serializeAsIndex; - _valuesByEnumNaming = valuesByEnumNaming; - _valuesByToString = valuesByToString; } /** * Factory method used by {@link tools.jackson.databind.ser.BasicSerializerFactory} * for constructing serializer instance of Enum types. */ - @SuppressWarnings("unchecked") public static EnumSerializer construct(Class enumClass, SerializationConfig config, BeanDescription beanDesc, JsonFormat.Value format) { // 08-Apr-2015, tatu: As per [databind#749], we cannot statically determine // between name() and toString(), need to construct `EnumValues` with names, // handle toString() case dynamically (for example) - EnumValues v = EnumValues.constructFromName(config, beanDesc.getClassInfo()); - EnumValues valuesByEnumNaming = constructEnumNamingStrategyValues(config, (Class>) enumClass, beanDesc.getClassInfo()); - EnumValues valuesByToString = EnumValues.constructFromToString(config, beanDesc.getClassInfo()); + // 26-Nov-2025, tatu: Further refactoring post-[databind#5432] to deprecate + // `EnumValues`, replaced with `EnumValuesToWrite` + EnumValuesToWrite writer = EnumDefinition.construct(config, beanDesc.getClassInfo()) + .valuesToWrite(config); Boolean serializeAsIndex = _isShapeWrittenUsingIndex(enumClass, format, true, null); - return new EnumSerializer(v, serializeAsIndex, valuesByEnumNaming, valuesByToString); + return new EnumSerializer(writer, serializeAsIndex); } /** @@ -103,8 +88,7 @@ public ValueSerializer createContextual(SerializationContext ctxt, Boolean serializeAsIndex = _isShapeWrittenUsingIndex(type, format, false, _serializeAsIndex); if (!Objects.equals(serializeAsIndex, _serializeAsIndex)) { - return new EnumSerializer(_values, serializeAsIndex, - _valuesByEnumNaming, _valuesByToString); + return new EnumSerializer(_enumValuesToWrite, serializeAsIndex); } } return this; @@ -116,7 +100,12 @@ public ValueSerializer createContextual(SerializationContext ctxt, /********************************************************************** */ - public EnumValues getEnumValues() { return _values; } + @Deprecated // @since 3.1 + public EnumValues getEnumValues() { + // 26-Nov-2025, tatu: Unfortunate, but can't really support getting + // such value, so better fail flamboyantly instead of quietly + throw new UnsupportedOperationException(); + } /* /********************************************************************** @@ -128,21 +117,17 @@ public ValueSerializer createContextual(SerializationContext ctxt, public final void serialize(Enum en, JsonGenerator g, SerializationContext ctxt) throws JacksonException { - if (_valuesByEnumNaming != null) { - g.writeString(_valuesByEnumNaming.serializedValueFor(en)); - return; - } // Serialize as index? if (_serializeAsIndex(ctxt)) { g.writeNumber(en.ordinal()); return; } - // [databind#749]: or via toString()? + final MapperConfig config = ctxt.getConfig(); if (ctxt.isEnabled(EnumFeature.WRITE_ENUMS_USING_TO_STRING)) { - g.writeString(_valuesByToString.serializedValueFor(en)); + g.writeString(_enumValuesToWrite.enumValueFromToString(config, en)); return; - } - g.writeString(_values.serializedValueFor(en)); + } + g.writeString(_enumValuesToWrite.enumValueFromName(config, en)); } /* @@ -154,28 +139,30 @@ public final void serialize(Enum en, JsonGenerator g, SerializationContext ct @Override public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) { - SerializationContext serializers = visitor.getContext(); - if (_serializeAsIndex(serializers)) { + SerializationContext ctxt = visitor.getContext(); + if (_serializeAsIndex(ctxt)) { visitIntFormat(visitor, typeHint, JsonParser.NumberType.INT); return; } JsonStringFormatVisitor stringVisitor = visitor.expectStringFormat(typeHint); if (stringVisitor != null) { - Set enums = new LinkedHashSet(); + Set enumStrings = new LinkedHashSet<>(); - // Use toString()? - if ((serializers != null) && - serializers.isEnabled(EnumFeature.WRITE_ENUMS_USING_TO_STRING)) { - for (SerializableString value : _valuesByToString.values()) { - enums.add(value.getValue()); + List> enums = _enumValuesToWrite.enums(); + if (_serializeAsIndex(ctxt)) { + for (Enum en : enums) { + enumStrings.add(String.valueOf(en.ordinal())); } } else { - // No, serialize using name() or explicit overrides - for (SerializableString value : _values.values()) { - enums.add(value.getValue()); + final MapperConfig config = ctxt.getConfig(); + SerializableString[] values = ctxt.isEnabled(EnumFeature.WRITE_ENUMS_USING_TO_STRING) + ? _enumValuesToWrite.allEnumValuesFromToString(config) + : _enumValuesToWrite.allEnumValuesFromName(config); + for (SerializableString sstr : values) { + enumStrings.add(sstr.getValue()); } } - stringVisitor.enumTypes(enums); + stringVisitor.enumTypes(enumStrings); } } @@ -222,17 +209,4 @@ protected static Boolean _isShapeWrittenUsingIndex(Class enumClass, "Unsupported serialization shape (%s) for Enum %s, not supported as %s annotation", shape, enumClass.getName(), (fromClass? "class" : "property"))); } - - /** - * Factory method used to resolve an instance of {@link EnumValues} - * with {@link EnumNamingStrategy} applied for the target class. - */ - protected static EnumValues constructEnumNamingStrategyValues(SerializationConfig config, Class> enumClass, - AnnotatedClass annotatedClass) { - Object namingDef = config.getAnnotationIntrospector().findEnumNamingStrategy(config, annotatedClass); - EnumNamingStrategy enumNamingStrategy = EnumNamingStrategyFactory.createEnumNamingStrategyInstance( - namingDef, config.canOverrideAccessModifiers(), config.getEnumNamingStrategy()); - return enumNamingStrategy == null ? null : EnumValues.constructUsingEnumNamingStrategy( - config, annotatedClass, enumNamingStrategy); - } } diff --git a/src/main/java/tools/jackson/databind/ser/jdk/JDKKeySerializers.java b/src/main/java/tools/jackson/databind/ser/jdk/JDKKeySerializers.java index dbcc6f8244..833f004b15 100644 --- a/src/main/java/tools/jackson/databind/ser/jdk/JDKKeySerializers.java +++ b/src/main/java/tools/jackson/databind/ser/jdk/JDKKeySerializers.java @@ -287,12 +287,11 @@ public void serialize(Object value, JsonGenerator g, SerializationContext ctxt) // 26-Nov-2025, tatu: In 3.0 order was opposite (TO_STRING first, // then INDEX); changed in 3.1 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())); } else if (ctxt.isEnabled(EnumFeature.WRITE_ENUMS_USING_TO_STRING)) { - g.writeName(_valuesToWrite.fromToString(ctxt.getConfig(), en)); + g.writeName(_valuesToWrite.enumValueFromToString(ctxt.getConfig(), en)); } else { - g.writeName(_valuesToWrite.fromName(ctxt.getConfig(), en)); + g.writeName(_valuesToWrite.enumValueFromName(ctxt.getConfig(), en)); } } } diff --git a/src/main/java/tools/jackson/databind/util/EnumValues.java b/src/main/java/tools/jackson/databind/util/EnumValues.java index 229e70cbfd..42eacdbb5a 100644 --- a/src/main/java/tools/jackson/databind/util/EnumValues.java +++ b/src/main/java/tools/jackson/databind/util/EnumValues.java @@ -10,8 +10,11 @@ /** * Helper class used for storing String serializations of {@code Enum}s, - * to match to/from external representations. + * to match to external representations. + * + * @deprecated Since 3.1 should no longer be used (replaced by {@link EnumValuesToWrite}). */ +@Deprecated public final class EnumValues implements java.io.Serializable { @@ -31,14 +34,6 @@ private EnumValues(Class> enumClass, SerializableString[] textual) _textual = 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); diff --git a/src/main/java/tools/jackson/databind/util/EnumValuesToWrite.java b/src/main/java/tools/jackson/databind/util/EnumValuesToWrite.java index d6e2aafdd7..4af14fa9d2 100644 --- a/src/main/java/tools/jackson/databind/util/EnumValuesToWrite.java +++ b/src/main/java/tools/jackson/databind/util/EnumValuesToWrite.java @@ -1,5 +1,7 @@ package tools.jackson.databind.util; +import java.util.Arrays; +import java.util.List; import java.util.function.Function; import tools.jackson.core.SerializableString; @@ -47,7 +49,21 @@ public static EnumValuesToWrite construct(MapperConfig config, enumNamingStrategy, enumConstants, explicitNames); } - public SerializableString fromName(MapperConfig config, Enum en) { + @SuppressWarnings("unchecked") + public Class> enumClass() { + Class cls = _annotatedClass.getRawType(); + return (Class>) cls; + } + + public List> enums() { + return Arrays.asList(_enumConstants); + } + + public SerializableString enumValueFromName(MapperConfig config, Enum en) { + return allEnumValuesFromName(config)[en.ordinal()]; + } + + public SerializableString[] allEnumValuesFromName(MapperConfig config) { SerializableString[] strs; if (config.isEnabled(EnumFeature.WRITE_ENUMS_TO_LOWERCASE)) { if ((strs = _enumNamesLC) == null) { @@ -62,21 +78,29 @@ public SerializableString fromName(MapperConfig config, Enum en) { false); } } - return strs[en.ordinal()]; + return strs; + } + + public SerializableString enumValueFromToString(MapperConfig config, Enum en) { + return allEnumValuesFromToString(config)[en.ordinal()]; } - public SerializableString fromToString(MapperConfig config, Enum en) { + public SerializableString[] allEnumValuesFromToString(MapperConfig config) { SerializableString[] strs; if (config.isEnabled(EnumFeature.WRITE_ENUMS_TO_LOWERCASE)) { if ((strs = _enumToStringsLC) == null) { - _enumToStringsLC = strs = _fetch(config, Enum::toString, true); + _enumToStringsLC = strs = _fetch(config, + e -> _toStringWithStrategy(config, e), + true); } } else { if ((strs = _enumToStrings) == null) { - _enumToStrings = strs = _fetch(config, Enum::toString, false); + _enumToStrings = strs = _fetch(config, + e -> _toStringWithStrategy(config, e), + false); } } - return strs[en.ordinal()]; + return strs; } private String _nameWithStrategy(MapperConfig config, Enum en) { @@ -87,6 +111,14 @@ private String _nameWithStrategy(MapperConfig config, Enum en) { return str; } + private String _toStringWithStrategy(MapperConfig config, Enum en) { + String str = en.toString(); + if (_enumNamingStrategy != null) { + str = _enumNamingStrategy.convertEnumToExternalName(config, _annotatedClass, str); + } + return str; + } + private SerializableString[] _fetch(MapperConfig config, Function,String> accessor, boolean lowerCase) { final int len = _enumConstants.length; diff --git a/src/test/java/tools/jackson/databind/jsonschema/NewSchemaTest.java b/src/test/java/tools/jackson/databind/jsonschema/NewSchemaTest.java index 69c19d784d..a75b2877d5 100644 --- a/src/test/java/tools/jackson/databind/jsonschema/NewSchemaTest.java +++ b/src/test/java/tools/jackson/databind/jsonschema/NewSchemaTest.java @@ -228,7 +228,7 @@ public void testBasicTraversal() throws Exception @Test public void testSimpleEnum() throws Exception { - final Set values = new TreeSet(); + final Set values = new TreeSet<>(); ObjectWriter w = MAPPER.writer(EnumFeature.WRITE_ENUMS_USING_TO_STRING); w.acceptJsonFormatVisitor(TestEnum.class, new JsonFormatVisitorWrapper.Base() { @@ -247,7 +247,7 @@ public void format(JsonValueFormat format) { } }); assertEquals(3, values.size()); - TreeSet exp = new TreeSet(Arrays.asList( + TreeSet exp = new TreeSet<>(Arrays.asList( "ToString:A", "ToString:B", "ToString:C" diff --git a/src/test/java/tools/jackson/databind/ser/enums/EnumNamingSerializationTest.java b/src/test/java/tools/jackson/databind/ser/enums/EnumNamingSerializationTest.java index 651c4d8672..d8967f6338 100644 --- a/src/test/java/tools/jackson/databind/ser/enums/EnumNamingSerializationTest.java +++ b/src/test/java/tools/jackson/databind/ser/enums/EnumNamingSerializationTest.java @@ -23,7 +23,7 @@ public class EnumNamingSerializationTest extends DatabindTestUtil .build(); @EnumNaming(EnumNamingStrategies.LowerCamelCaseStrategy.class) - static enum EnumFlavorA { + enum EnumFlavorA { CHOCOLATE_CHIPS, HOT_CHEETOS; @@ -34,26 +34,26 @@ public String toString() { } @EnumNaming(EnumNamingStrategies.LowerCamelCaseStrategy.class) - static enum EnumSauceB { + enum EnumSauceB { KETCH_UP, MAYO_NEZZ; } @EnumNaming(EnumNamingStrategy.class) - static enum EnumSauceC { + enum EnumSauceC { BARBEQ_UE, SRIRACHA_MAYO; } @EnumNaming(EnumNamingStrategies.LowerCamelCaseStrategy.class) - static enum EnumFlavorD { + enum EnumFlavorD { _PEANUT_BUTTER, PEANUT__BUTTER, PEANUT_BUTTER } @EnumNaming(EnumNamingStrategies.LowerCamelCaseStrategy.class) - static enum EnumFlavorE { + enum EnumFlavorE { PEANUT_BUTTER, @JsonProperty("almond") ALMOND_BUTTER @@ -75,28 +75,30 @@ public EnumFlavorWrapperBean(@JsonProperty("sce") EnumSauceB sce) { */ @Test - public void testEnumNamingShouldOverrideToStringFeatue() throws Exception { + public void enumNamingShouldOverrideToStringFeature() throws Exception { String resultStr = MAPPER.writer() .with(EnumFeature.WRITE_ENUMS_USING_TO_STRING) .writeValueAsString(EnumFlavorA.CHOCOLATE_CHIPS); - assertEquals(q("chocolateChips"), resultStr); + // 26-Nov-2025, tatu: Before 3.1, test assumed that "WRITE_ENUMS_USING_TO_STRING" + // prevents use of EnumNamingStrategy -- not so with 3.1 and later + assertEquals(q("hotChocolateCheetosAndChips"), resultStr); } @Test - public void testEnumNamingStrategyNotApplied() throws Exception { + public void enumNamingStrategyNotApplied() throws Exception { String resultString = MAPPER.writeValueAsString(EnumSauceC.SRIRACHA_MAYO); assertEquals(q("SRIRACHA_MAYO"), resultString); } @Test - public void testEnumNamingStrategyStartingUnderscoreBecomesUpperCase() throws Exception { + public void enumNamingStrategyStartingUnderscoreBecomesUpperCase() throws Exception { String flavor = MAPPER.writeValueAsString(EnumFlavorD._PEANUT_BUTTER); assertEquals(q("PeanutButter"), flavor); } @Test - public void testEnumNamingStrategyNonPrefixContiguousUnderscoresBecomeOne() throws Exception { + public void enumNamingStrategyNonPrefixContiguousUnderscoresBecomeOne() throws Exception { String flavor1 = MAPPER.writeValueAsString(EnumFlavorD.PEANUT__BUTTER); assertEquals(q("peanutButter"), flavor1); @@ -105,13 +107,13 @@ public void testEnumNamingStrategyNonPrefixContiguousUnderscoresBecomeOne() thro } @Test - public void testEnumSet() throws Exception { + public void enumSetWrite() throws Exception { final EnumSet value = EnumSet.of(EnumSauceB.KETCH_UP); assertEquals("[\"ketchUp\"]", MAPPER.writeValueAsString(value)); } @Test - public void testEnumWithEnumMap() throws Exception { + public void enumMapWrite() throws Exception { EnumMap enums = new EnumMap<>(EnumSauceB.class); enums.put(EnumSauceB.MAYO_NEZZ, "value"); @@ -123,7 +125,7 @@ public void testEnumWithEnumMap() throws Exception { } @Test - public void testEnumNamingStrategyWithOverride() throws Exception { + public void enumNamingStrategyWithOverride() throws Exception { String almond = MAPPER.writeValueAsString(EnumFlavorE.ALMOND_BUTTER); assertEquals(q("almond"), almond); diff --git a/src/test/java/tools/jackson/databind/util/EnumValuesTest.java b/src/test/java/tools/jackson/databind/util/EnumValuesTest.java index 543d0053bf..9fa8e94f57 100644 --- a/src/test/java/tools/jackson/databind/util/EnumValuesTest.java +++ b/src/test/java/tools/jackson/databind/util/EnumValuesTest.java @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +@Deprecated // @since 3.1 along with EnumValues type itself public class EnumValuesTest extends DatabindTestUtil { enum ABC {