From 73d3bbdbe8d3f5ebd53883bf9bc844d9663a87c0 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 27 Nov 2023 20:28:08 -0800 Subject: [PATCH] Fix #4200: use annotations for delegating `@JsonCreator` (#4228) --- release-notes/VERSION-2.x | 4 +- .../databind/deser/BeanDeserializerBase.java | 84 +++++++++++++++++-- .../NullConversionsForContent4200Test.java | 63 ++++++++++++++ 3 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 src/test/java/com/fasterxml/jackson/databind/deser/filter/NullConversionsForContent4200Test.java diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 7d7e758c44..f377855960 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -6,10 +6,10 @@ Project: jackson-databind 2.17.0 (not yet released) -- - 2.16.1 (not yet released) +#4200: `JsonSetter(contentNulls = FAIL)` is ignored in delegating + `@JsonCreator` argument #4216: Primitive array deserializer cannot being captured by `DeserializerModifier` (reported by @SakuraKoi) (fix contributed by Joo-Hyuk K) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java index 953f9b7af7..8398c71b7e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.core.JsonParser.NumberType; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.cfg.ConfigOverride; import com.fasterxml.jackson.databind.deser.impl.*; import com.fasterxml.jackson.databind.deser.std.StdDelegatingDeserializer; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; @@ -695,12 +696,29 @@ protected void _replaceProperty(BeanPropertyMap props, SettableBeanProperty[] cr @SuppressWarnings("unchecked") private JsonDeserializer _findDelegateDeserializer(DeserializationContext ctxt, - JavaType delegateType, AnnotatedWithParams delegateCreator) throws JsonMappingException + JavaType delegateType, AnnotatedWithParams delegateCreator) + throws JsonMappingException { - // Need to create a temporary property to allow contextual deserializers: - BeanProperty.Std property = new BeanProperty.Std(TEMP_PROPERTY_NAME, - delegateType, null, delegateCreator, - PropertyMetadata.STD_OPTIONAL); + // 27-Nov-2023, tatu: [databind#4200] Need to resolve PropertyMetadata. + // And all we have is the actual Creator method; but for annotations + // we actually need the one parameter -- if there is one + // (NOTE! This would not work for case of more than one parameter with + // delegation, others injected) + final BeanProperty property; + + if ((delegateCreator != null) && (delegateCreator.getParameterCount() == 1)) { + AnnotatedMember delegator = delegateCreator.getParameter(0); + PropertyMetadata propMd = _getSetterInfo(ctxt, delegator, delegateType); + property = new BeanProperty.Std(TEMP_PROPERTY_NAME, + delegateType, null, delegator, propMd); + } else { + // No creator indicated; or Zero, or more than 2 arguments (since we don't + // know which one is the "real" delegating parameter. Although could possibly + // figure it out if someone provides actual use case + property = new BeanProperty.Std(TEMP_PROPERTY_NAME, + delegateType, null, delegateCreator, + PropertyMetadata.STD_OPTIONAL); + } TypeDeserializer td = delegateType.getTypeHandler(); if (td == null) { td = ctxt.getConfig().findTypeDeserializer(delegateType); @@ -720,6 +738,62 @@ private JsonDeserializer _findDelegateDeserializer(DeserializationContex return dd; } + /** + * Method essentially copied from {@code BasicDeserializerFactory}, + * needed to find {@link PropertyMetadata} for Delegating Creator, + * for access to annotation-derived info. + * + * @since 2.16.1 + */ + protected PropertyMetadata _getSetterInfo(DeserializationContext ctxt, + AnnotatedMember accessor, JavaType type) + { + final AnnotationIntrospector intr = ctxt.getAnnotationIntrospector(); + final DeserializationConfig config = ctxt.getConfig(); + + PropertyMetadata metadata = PropertyMetadata.STD_OPTIONAL; + boolean needMerge = true; + Nulls valueNulls = null; + Nulls contentNulls = null; + + // NOTE: compared to `POJOPropertyBuilder`, we only have access to creator + // parameter, not other accessors, so code bit simpler + // Ok, first: does property itself have something to say? + if (intr != null) { + JsonSetter.Value setterInfo = intr.findSetterInfo(accessor); + if (setterInfo != null) { + valueNulls = setterInfo.nonDefaultValueNulls(); + contentNulls = setterInfo.nonDefaultContentNulls(); + } + } + // If not, config override? + if (needMerge || (valueNulls == null) || (contentNulls == null)) { + ConfigOverride co = config.getConfigOverride(type.getRawClass()); + JsonSetter.Value setterInfo = co.getSetterInfo(); + if (setterInfo != null) { + if (valueNulls == null) { + valueNulls = setterInfo.nonDefaultValueNulls(); + } + if (contentNulls == null) { + contentNulls = setterInfo.nonDefaultContentNulls(); + } + } + } + if (needMerge || (valueNulls == null) || (contentNulls == null)) { + JsonSetter.Value setterInfo = config.getDefaultSetterInfo(); + if (valueNulls == null) { + valueNulls = setterInfo.nonDefaultValueNulls(); + } + if (contentNulls == null) { + contentNulls = setterInfo.nonDefaultContentNulls(); + } + } + if ((valueNulls != null) || (contentNulls != null)) { + metadata = metadata.withNulls(valueNulls, contentNulls); + } + return metadata; + } + /** * Helper method that can be used to see if specified property is annotated * to indicate use of a converter for property value (in case of container types, diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/filter/NullConversionsForContent4200Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/filter/NullConversionsForContent4200Test.java new file mode 100644 index 0000000000..b4771a1ae3 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/filter/NullConversionsForContent4200Test.java @@ -0,0 +1,63 @@ +package com.fasterxml.jackson.databind.deser.filter; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.*; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.exc.InvalidNullException; + +// [databind#4200]: Nulls.FAIL not taken into account with DELEGATING creator +public class NullConversionsForContent4200Test extends BaseMapTest +{ + static class DelegatingWrapper4200 { + private final Map value; + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + DelegatingWrapper4200(@JsonSetter(contentNulls = Nulls.FAIL) + Map value) + { + this.value = value; + } + + public Map getValue() { + return value; + } + } + + static class SetterWrapper4200 { + private Map value; + + public Map getValue() { + return value; + } + + @JsonSetter(contentNulls = Nulls.FAIL) + public void setValue(Map value) { + this.value = value; + } + } + + private final ObjectMapper MAPPER = newJsonMapper(); + + public void testDelegatingCreatorNulls4200() throws Exception + { + try { + MAPPER.readValue(a2q("{'foo': null}"), DelegatingWrapper4200.class); + fail("Should not pass"); + } catch (InvalidNullException e) { + verifyException(e, "Invalid `null` value"); + } + } + + public void testSetterNulls4200() throws Exception + { + try { + MAPPER.readValue(a2q("{'value':{'foo': null}}"), + SetterWrapper4200.class); + fail("Should not pass"); + } catch (InvalidNullException e) { + verifyException(e, "Invalid `null` value"); + } + } +}