Skip to content

fix(#4218): avoid duplicate injection on fields #5224

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: 2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,9 @@ protected void addInjectables(DeserializationContext ctxt,

for (Map.Entry<Object, AnnotatedMember> entry : raw.entrySet()) {
AnnotatedMember m = entry.getValue();
if (m.isIgnoreInjection()) {
Copy link
Contributor Author

@giulong giulong Jul 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We avoid creating a redundant ValueInjector for fields whose injection was already performed via constructor properties.

continue;
}
final JacksonInject.Value injectableValue = introspector.findInjectableValue(m);
final Boolean optional = injectableValue == null ? null : injectableValue.getOptional();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<String, POJOPropertyBuilder> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.fasterxml.jackson.databind.tofix;
package com.fasterxml.jackson.databind.deser.inject;

import java.util.Objects;

Expand All @@ -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;

Expand Down Expand Up @@ -41,7 +40,6 @@ public String getField2() {
}

// [databind#2678]
@JacksonTestFailureExpected
@Test
void readValueInjectables() throws Exception {
final InjectableValues injectableValues =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.fasterxml.jackson.databind.tofix;
package com.fasterxml.jackson.databind.deser.inject;

import org.junit.jupiter.api.Test;

Expand All @@ -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;

Expand Down Expand Up @@ -51,7 +50,6 @@ public Object findInjectableValue(
}

// [databind#4218]
@JacksonTestFailureExpected
@Test
void injectFail4218() throws Exception
{
Expand Down
Loading