diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index bf74f6e317..1ff4e891c9 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -821,6 +821,9 @@ protected void addInjectables(DeserializationContext ctxt, for (Map.Entry entry : raw.entrySet()) { AnnotatedMember m = entry.getValue(); + if (m.isIgnoreInjection()) { + continue; + } final JacksonInject.Value injectableValue = introspector.findInjectableValue(m); final Boolean optional = injectableValue == null ? null : injectableValue.getOptional(); diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotatedMember.java b/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotatedMember.java index e2da0c8cca..c8f5eae63b 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotatedMember.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotatedMember.java @@ -32,6 +32,13 @@ public abstract class AnnotatedMember // no need to persist protected final transient AnnotationMap _annotations; + /** + * Flag to avoid duplicate injection. See issue #4218 + * + * @since 2.20 + */ + protected boolean _ignoreInjection; + protected AnnotatedMember(TypeResolutionContext ctxt, AnnotationMap annotations) { super(); _typeContext = ctxt; @@ -140,6 +147,20 @@ public final void fixAccess(boolean force) { } } + /** + * @since 2.20 + */ + public void ignoreInjection() { + _ignoreInjection = true; + } + + /** + * @since 2.20 + */ + public boolean isIgnoreInjection() { + return _ignoreInjection; + } + /** * Optional method that can be used to assign value of * this member on given object, if this is a supported diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java index e921f61913..eaa3b17dba 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java @@ -450,6 +450,8 @@ protected void collectAll() if (_config.isEnabled(MapperFeature.FIX_FIELD_NAME_UPPER_CASE_PREFIX)) { _fixLeadingFieldNameCase(props); } + // Mark injected fields that are already injected via constructor properties + _ignoreDuplicateInjection(props); // Remove ignored properties, first; this MUST precede annotation merging // since logic relies on knowing exactly which accessor has which annotation _removeUnwantedProperties(props); @@ -1417,6 +1419,40 @@ private boolean _firstOrSecondCharUpperCase(String name) { /********************************************************** */ + /** + * Method to mark injected fields as ignored if there's a corresponding + * creator property already injecting the same value + */ + protected void _ignoreDuplicateInjection(final Map props) + { + for (POJOPropertyBuilder prop : props.values()) { + final AnnotatedField field = prop.getFieldUnchecked(); + if (field == null) { + continue; + } + + final JacksonInject.Value injectableValue = + _annotationIntrospector.findInjectableValue(field); + if (injectableValue == null) { + continue; + } + + for (POJOPropertyBuilder creatorProperty : _creatorProperties) { + if (creatorProperty == null) { + continue; + } + + final AnnotatedParameter parameter = creatorProperty.getConstructorParameter(); + if (parameter != null + && injectableValue.equals( + _annotationIntrospector.findInjectableValue(parameter))) { + field.ignoreInjection(); + break; + } + } + } + } + /** * Method called to get rid of candidate properties that are marked * as ignored. diff --git a/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordBasicsTest.java b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordBasicsTest.java index 519f52dbc6..93f7af784f 100644 --- a/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordBasicsTest.java +++ b/src/test-jdk17/java/com/fasterxml/jackson/databind/records/RecordBasicsTest.java @@ -179,29 +179,16 @@ public void testDeserializeJsonRename() throws Exception { assertEquals(new RecordWithRename(123, "Bob"), value); } - /** - * This test-case is just for documentation purpose: - * GOTCHA: Annotations on header will be propagated to the field, leading to this failure. - * - * @see #testDeserializeConstructorInjectRecord() - */ + // Confirmation of fix of [databind#4218] @Test - public void testDeserializeHeaderInjectRecord_WillFail() throws Exception { + public void testDeserializeHeaderInjectRecord4218() throws Exception { ObjectReader reader = MAPPER.readerFor(RecordWithHeaderInject.class) .with(new InjectableValues.Std().addValue(String.class, "Bob")); - - try { - reader.readValue("{\"id\":123}"); - - fail("should not pass"); - } catch (IllegalArgumentException e) { - verifyException(e, "RecordWithHeaderInject#name"); - verifyException(e, "Can not set final java.lang.String field"); - } + assertNotNull(reader.readValue("{\"id\":123}")); } @Test - public void testDeserializeConstructorInjectRecord() throws Exception { + public void testDeserializeConstructorInjectRecord4218() throws Exception { ObjectReader reader = MAPPER.readerFor(RecordWithConstructorInject.class) .with(new InjectableValues.Std().addValue(String.class, "Bob")); RecordWithConstructorInject value = reader.readValue("{\"id\":123}"); diff --git a/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject2678Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject2678Test.java similarity index 93% rename from src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject2678Test.java rename to src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject2678Test.java index 20f1d41abc..4d352724b9 100644 --- a/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject2678Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject2678Test.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.databind.tofix; +package com.fasterxml.jackson.databind.deser.inject; import java.util.Objects; @@ -10,7 +10,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; -import com.fasterxml.jackson.databind.testutil.failure.JacksonTestFailureExpected; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -41,7 +40,6 @@ public String getField2() { } // [databind#2678] - @JacksonTestFailureExpected @Test void readValueInjectables() throws Exception { final InjectableValues injectableValues = diff --git a/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject4218Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject4218Test.java similarity index 91% rename from src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject4218Test.java rename to src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject4218Test.java index 473f089598..c406337a9d 100644 --- a/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject4218Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject4218Test.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.databind.tofix; +package com.fasterxml.jackson.databind.deser.inject; import org.junit.jupiter.api.Test; @@ -8,7 +8,6 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; -import com.fasterxml.jackson.databind.testutil.failure.JacksonTestFailureExpected; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -51,7 +50,6 @@ public Object findInjectableValue( } // [databind#4218] - @JacksonTestFailureExpected @Test void injectFail4218() throws Exception {