From 143b66fcf6bdbac4f7351bd1e21d1c48fb82d424 Mon Sep 17 00:00:00 2001 From: Will Paul Date: Tue, 18 Feb 2025 13:01:32 -0500 Subject: [PATCH 1/6] Allow default enums with @JsonCreator This follows the pattern for READ_UNKNOWN_ENUM_VALUES_AS_NULL from here: https://github.com/FasterXML/jackson-databind/pull/1642/files but for READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE It's more tricky because it wasn't immediately apparent how to get the configured default value on the Creator impl side. I decided to try using the EnumResolver, which works, but am not sure what the repercussions of that are fully though. It works the same way, so only IllegalArgumentExceptions will trigger default behavior, so if you have some other custom creator exception logic that will be unaffected. --- .../deser/BasicDeserializerFactory.java | 4 +++- .../databind/deser/std/EnumDeserializer.java | 11 +++++------ .../std/FactoryBasedEnumDeserializer.java | 19 ++++++++++++++++++- .../deser/creators/EnumCreatorTest.java | 3 ++- .../deser/enums/EnumDeserializationTest.java | 17 +++++++++++++++-- 5 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java index 1a71004792..d523e7dd63 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java @@ -1087,7 +1087,9 @@ public JsonDeserializer createEnumDeserializer(DeserializationContext ctxt, "Invalid `@JsonCreator` annotated Enum factory method [%s]: needs to return compatible type", factory.toString())); } - deser = EnumDeserializer.deserializerForCreator(config, enumClass, factory, valueInstantiator, creatorProps); + deser = EnumDeserializer.deserializerForCreator( + config, enumClass, factory, valueInstantiator, creatorProps, + constructEnumResolver(enumClass, config, beanDesc)); break; } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java index a2973e3930..22d7509a61 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java @@ -160,7 +160,7 @@ public EnumDeserializer(EnumResolver byNameResolver) { @Deprecated public static JsonDeserializer deserializerForCreator(DeserializationConfig config, Class enumClass, AnnotatedMethod factory) { - return deserializerForCreator(config, enumClass, factory, null, null); + return deserializerForCreator(config, enumClass, factory, null, null, null); } /** @@ -172,16 +172,15 @@ public static JsonDeserializer deserializerForCreator(DeserializationConfig c * @since 2.8 */ public static JsonDeserializer deserializerForCreator(DeserializationConfig config, - Class enumClass, AnnotatedMethod factory, - ValueInstantiator valueInstantiator, SettableBeanProperty[] creatorProps) + Class enumClass, AnnotatedMethod factory, ValueInstantiator valueInstantiator, + SettableBeanProperty[] creatorProps, EnumResolver byNameResolver) { if (config.canOverrideAccessModifiers()) { ClassUtil.checkAndFixAccess(factory.getMember(), config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS)); } - return new FactoryBasedEnumDeserializer(enumClass, factory, - factory.getParameterType(0), - valueInstantiator, creatorProps); + return new FactoryBasedEnumDeserializer(enumClass, factory, factory.getParameterType(0), + valueInstantiator, creatorProps, byNameResolver); } /** diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java index 3f01110f38..f01753dbad 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java @@ -1,6 +1,7 @@ package com.fasterxml.jackson.databind.deser.std; import java.io.IOException; +import java.util.Objects; import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonParser; @@ -15,6 +16,7 @@ import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.ClassUtil; +import com.fasterxml.jackson.databind.util.EnumResolver; /** * Deserializer that uses a single-String static factory method @@ -34,6 +36,7 @@ class FactoryBasedEnumDeserializer protected final JsonDeserializer _deser; protected final ValueInstantiator _valueInstantiator; protected final SettableBeanProperty[] _creatorProps; + protected final Enum _defaultValue; protected final boolean _hasArgs; @@ -45,7 +48,8 @@ class FactoryBasedEnumDeserializer private transient volatile PropertyBasedCreator _propCreator; public FactoryBasedEnumDeserializer(Class cls, AnnotatedMethod f, JavaType paramType, - ValueInstantiator valueInstantiator, SettableBeanProperty[] creatorProps) + ValueInstantiator valueInstantiator, SettableBeanProperty[] creatorProps, + EnumResolver enumResolver) { super(cls); _factory = f; @@ -56,6 +60,13 @@ public FactoryBasedEnumDeserializer(Class cls, AnnotatedMethod f, JavaType pa _deser = null; _valueInstantiator = valueInstantiator; _creatorProps = creatorProps; + _defaultValue = Objects.nonNull(enumResolver)? enumResolver.getDefaultValue() : null; + } + + public FactoryBasedEnumDeserializer(Class cls, AnnotatedMethod f, JavaType paramType, + ValueInstantiator valueInstantiator, SettableBeanProperty[] creatorProps) + { + this(cls, f, paramType, valueInstantiator, creatorProps, null); } /** @@ -70,6 +81,7 @@ public FactoryBasedEnumDeserializer(Class cls, AnnotatedMethod f) _deser = null; _valueInstantiator = null; _creatorProps = null; + _defaultValue = null; } protected FactoryBasedEnumDeserializer(FactoryBasedEnumDeserializer base, @@ -80,6 +92,7 @@ protected FactoryBasedEnumDeserializer(FactoryBasedEnumDeserializer base, _hasArgs = base._hasArgs; _valueInstantiator = base._valueInstantiator; _creatorProps = base._creatorProps; + _defaultValue = base._defaultValue; _deser = deser; } @@ -202,6 +215,10 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx if (ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)) { return null; } + + if (ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)) { + return _defaultValue; + } // 12-Oct-2021, tatu: Should probably try to provide better exception since // we likely hit argument incompatibility... Or can this happen? } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/creators/EnumCreatorTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/creators/EnumCreatorTest.java index 4aaf6d8c86..2a5bacefee 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/creators/EnumCreatorTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/creators/EnumCreatorTest.java @@ -125,7 +125,8 @@ public JsonDeserializer findEnumDeserializer(final Class type, final Deser for (AnnotatedMethod am : factoryMethods) { final JsonCreator creator = am.getAnnotation(JsonCreator.class); if (creator != null) { - return EnumDeserializer.deserializerForCreator(config, type, am, null, null); + return EnumDeserializer.deserializerForCreator( + config, type, am, null, null, null); } } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/enums/EnumDeserializationTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/enums/EnumDeserializationTest.java index 18a7f9108b..f27ca98df5 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/enums/EnumDeserializationTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/enums/EnumDeserializationTest.java @@ -161,7 +161,7 @@ static enum EnumWithDefaultAnnoAndConstructor { } static enum StrictEnumCreator { - A, B; + A, B, @JsonEnumDefaultValue UNKNOWN; @JsonCreator public static StrictEnumCreator fromId(String value) { for (StrictEnumCreator e: values()) { @@ -453,7 +453,7 @@ public void testAllowUnknownEnumValuesReadAsNull() throws Exception assertNull(reader.forType(TestEnum.class).readValue(" 4343 ")); } - // Ability to ignore unknown Enum values: + // Ability to ignore unknown Enum values as null: // [databind#1642] @Test @@ -483,6 +483,19 @@ public void testAllowUnknownEnumValuesAsMapKeysReadAsNull() throws Exception assertTrue(result.map.containsKey(null)); } + // Ability to ignore unknown Enum values as a defined default: + + // [databind#4979] + @Test + public void testAllowUnknownEnumValuesReadAsDefaultWithCreatorMethod4979(() throws Exception + { + ObjectReader reader = MAPPER.reader( + DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE); + assertEquals( + StrictEnumCreator.UNKNOWN, + reader.forType(StrictEnumCreator.class).readValue("\"NO-SUCH-VALUE\"")); + } + @Test public void testDoNotAllowUnknownEnumValuesAsMapKeysWhenReadAsNullDisabled() throws Exception { From 04f52ac0942c2b24d2a7981810b4dfc979603f5a Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 12 Mar 2025 16:41:37 -0700 Subject: [PATCH 2/6] Remove extra character --- .../jackson/databind/deser/enums/EnumDeserializationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/enums/EnumDeserializationTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/enums/EnumDeserializationTest.java index f27ca98df5..94814c3c30 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/enums/EnumDeserializationTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/enums/EnumDeserializationTest.java @@ -487,7 +487,7 @@ public void testAllowUnknownEnumValuesAsMapKeysReadAsNull() throws Exception // [databind#4979] @Test - public void testAllowUnknownEnumValuesReadAsDefaultWithCreatorMethod4979(() throws Exception + public void testAllowUnknownEnumValuesReadAsDefaultWithCreatorMethod4979() throws Exception { ObjectReader reader = MAPPER.reader( DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE); From fb481b6b94dbe91083df02a87a17ccf12b564640 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 12 Mar 2025 16:50:32 -0700 Subject: [PATCH 3/6] Add deprecated marker, re-order (to minimize diffs) --- .../std/FactoryBasedEnumDeserializer.java | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java index f01753dbad..305ea7f122 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java @@ -1,7 +1,6 @@ package com.fasterxml.jackson.databind.deser.std; import java.io.IOException; -import java.util.Objects; import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonParser; @@ -47,6 +46,20 @@ class FactoryBasedEnumDeserializer */ private transient volatile PropertyBasedCreator _propCreator; + /** + * @since 2.8 + * @deprecated since 2.19 + */ + @Deprecated + public FactoryBasedEnumDeserializer(Class cls, AnnotatedMethod f, JavaType paramType, + ValueInstantiator valueInstantiator, SettableBeanProperty[] creatorProps) + { + this(cls, f, paramType, valueInstantiator, creatorProps, null); + } + + /** + * @since 2.19 + */ public FactoryBasedEnumDeserializer(Class cls, AnnotatedMethod f, JavaType paramType, ValueInstantiator valueInstantiator, SettableBeanProperty[] creatorProps, EnumResolver enumResolver) @@ -60,13 +73,7 @@ public FactoryBasedEnumDeserializer(Class cls, AnnotatedMethod f, JavaType pa _deser = null; _valueInstantiator = valueInstantiator; _creatorProps = creatorProps; - _defaultValue = Objects.nonNull(enumResolver)? enumResolver.getDefaultValue() : null; - } - - public FactoryBasedEnumDeserializer(Class cls, AnnotatedMethod f, JavaType paramType, - ValueInstantiator valueInstantiator, SettableBeanProperty[] creatorProps) - { - this(cls, f, paramType, valueInstantiator, creatorProps, null); + _defaultValue = (enumResolver == null) ? null : enumResolver.getDefaultValue(); } /** From 614fdb4b6bd66ec9bb986c04beedd6c51b0da79b Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 12 Mar 2025 16:55:09 -0700 Subject: [PATCH 4/6] More deprecation markers etc --- .../databind/deser/std/EnumDeserializer.java | 29 ++++++++++--------- .../std/FactoryBasedEnumDeserializer.java | 2 ++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java index 22d7509a61..9320560c27 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java @@ -147,20 +147,20 @@ protected EnumDeserializer(EnumDeserializer base, Boolean caseInsensitive) { } /** - * @deprecated Since 2.9 - */ - @Deprecated - public EnumDeserializer(EnumResolver byNameResolver) { - this(byNameResolver, null); - } - - /** - * @deprecated Since 2.8 + * Factory method used when Enum instances are to be deserialized + * using a creator (static factory method) + * + * @return Deserializer based on given factory method + * + * @since 2.8 + * @deprecated Since 2.19 */ @Deprecated public static JsonDeserializer deserializerForCreator(DeserializationConfig config, - Class enumClass, AnnotatedMethod factory) { - return deserializerForCreator(config, enumClass, factory, null, null, null); + Class enumClass, AnnotatedMethod factory, + ValueInstantiator valueInstantiator, SettableBeanProperty[] creatorProps) { + return deserializerForCreator(config, enumClass, factory, valueInstantiator, + creatorProps, null); } /** @@ -169,11 +169,12 @@ public static JsonDeserializer deserializerForCreator(DeserializationConfig c * * @return Deserializer based on given factory method * - * @since 2.8 + * @since 2.19 */ public static JsonDeserializer deserializerForCreator(DeserializationConfig config, - Class enumClass, AnnotatedMethod factory, ValueInstantiator valueInstantiator, - SettableBeanProperty[] creatorProps, EnumResolver byNameResolver) + Class enumClass, AnnotatedMethod factory, + ValueInstantiator valueInstantiator, SettableBeanProperty[] creatorProps, + EnumResolver byNameResolver) { if (config.canOverrideAccessModifiers()) { ClassUtil.checkAndFixAccess(factory.getMember(), diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java index 305ea7f122..853e856cf7 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java @@ -35,6 +35,8 @@ class FactoryBasedEnumDeserializer protected final JsonDeserializer _deser; protected final ValueInstantiator _valueInstantiator; protected final SettableBeanProperty[] _creatorProps; + + // @since 2.19 protected final Enum _defaultValue; protected final boolean _hasArgs; From 4f84e9d56e99686ed52f85473691939ccf654c18 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 13 Mar 2025 16:54:26 -0700 Subject: [PATCH 5/6] Minor tweak to handling --- .../deser/std/FactoryBasedEnumDeserializer.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java index 853e856cf7..09f16946a6 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/FactoryBasedEnumDeserializer.java @@ -220,14 +220,17 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx } catch (Exception e) { Throwable t = ClassUtil.throwRootCauseIfIOE(e); if (t instanceof IllegalArgumentException) { - // [databind#1642]: + // [databind#4979]: unknown as default + if (ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)) { + // ... only if we DO have a default + if (_defaultValue != null) { + return _defaultValue; + } + } + // [databind#1642]: unknown as null if (ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)) { return null; } - - if (ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)) { - return _defaultValue; - } // 12-Oct-2021, tatu: Should probably try to provide better exception since // we likely hit argument incompatibility... Or can this happen? } From 23049187bd6ebfc5c4fd431e365b19b5bea2ca61 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Thu, 13 Mar 2025 16:56:22 -0700 Subject: [PATCH 6/6] Update release notes --- release-notes/CREDITS-2.x | 4 ++++ release-notes/VERSION-2.x | 2 ++ 2 files changed, 6 insertions(+) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 13d37dda39..a0403cee4c 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -1918,3 +1918,7 @@ Floris Westerman (@FWest98) Joren Inghelbrecht (@jin-harmoney) * Contributed #4953: Allow clearing all caches to avoid classloader leaks (2.19.0) + +Will Paul (@dropofwill) + * Contributed #4979: Allow default enums with `@JsonCreator` + (2.19.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 573438c014..7b894cedcc 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -66,6 +66,8 @@ Project: jackson-databind (fix by Joo-Hyuk K) #4963: Serializing `Map.Entry` as Bean with `@JsonFormat.shape = Shape.OBJECT` fails on JDK 17+ +#4979: Allow default enums with `@JsonCreator` + (contributed by Will P) #4997: `ObjectNode` put methods should do null check for key #5006: Add `MapperFeature.REQUIRE_HANDLERS_FOR_JAVA8_OPTIONALS` to prevent failure of `java.util.Optional` (de)serialization without Java 8 module