From facb3bce58e9ef3128dc6cac74e3e97f34e523d2 Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Sun, 13 Jul 2025 22:13:17 +0200 Subject: [PATCH] fix(#4218): avoid duplicate injection on fields when a corresponding injected constructor parameter is available --- .../deser/BeanDeserializerFactory.java | 3 ++ .../databind/introspect/AnnotatedMember.java | 21 +++++++++++ .../introspect/POJOPropertiesCollector.java | 37 +++++++++++++++++++ .../databind/records/RecordBasicsTest.java | 10 +---- .../inject}/JacksonInject2678Test.java | 4 +- .../inject}/JacksonInject4218Test.java | 4 +- 6 files changed, 64 insertions(+), 15 deletions(-) rename src/test/java/com/fasterxml/jackson/databind/{tofix => deser/inject}/JacksonInject2678Test.java (93%) rename src/test/java/com/fasterxml/jackson/databind/{tofix => deser/inject}/JacksonInject4218Test.java (91%) 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 c90bdef271..21d252331a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -816,6 +816,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 2e1bc148c0..9222de3700 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java @@ -447,6 +447,9 @@ protected void collectAll() _addCreators(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); @@ -1290,6 +1293,40 @@ private String _checkRenameByField(String implName) { /********************************************************** */ + /** + * 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 afafaa4734..74aafada29 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 @@ -188,15 +188,7 @@ public void testDeserializeJsonRename() throws Exception { @Test public void testDeserializeHeaderInjectRecord_WillFail() throws Exception { MAPPER.setInjectableValues(new InjectableValues.Std().addValue(String.class, "Bob")); - - try { - MAPPER.readValue("{\"id\":123}", RecordWithHeaderInject.class); - - fail("should not pass"); - } catch (IllegalArgumentException e) { - verifyException(e, "RecordWithHeaderInject#name"); - verifyException(e, "Can not set final java.lang.String field"); - } + MAPPER.readValue("{\"id\":123}", RecordWithHeaderInject.class); } @Test 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 {