Skip to content
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 @@ -779,6 +779,14 @@ protected List<BeanPropertyDefinition> filterBeanProps(DeserializationContext ct
continue;
}
if (!property.hasConstructorParameter()) { // never skip constructor params
// [databind#6201]: `@JacksonInject(useInput=OptBoolean.FALSE)` means value
// from input is to be ignored; drop the mutator so nothing can bind it
// (`ValueInjector` still assigns the injected value)
if (_isInjectOnlyMutator(ctxt, beanDescRef.get(), property)) {
// important: make ignorable, to avoid errors if value is actually seen
builder.addIgnorable(name);
continue;
}
Class<?> rawPropertyType = property.getRawPrimaryType();
// Some types are declared as ignorable as well
if ((rawPropertyType != null)
Expand All @@ -793,6 +801,39 @@ && isIgnorableType(ctxt, property, rawPropertyType, ignoredTypes)) {
return result;
}

/**
* Helper method for [databind#6201]: checks whether given property is backed by a
* Field or Setter annotated with {@code @JacksonInject(useInput = OptBoolean.FALSE)},
* that is, one for which the value from input must be ignored in favor of the
* injected value.
*<p>
* Creator properties enforce this themselves, via
* {@link tools.jackson.databind.deser.CreatorProperty#isInjectionOnly()}, and are
* excluded by the caller. Field- and Setter-backed properties have no equivalent
* check in the property loops, and since injection runs before properties are
* bound, a matching value from input would simply overwrite the injected one.
*
* @since 3.3
*/
private boolean _isInjectOnlyMutator(DeserializationContext ctxt,
BeanDescription beanDesc, BeanPropertyDefinition property)
{
AnnotatedMember mutator = property.getNonConstructorMutator();
if (mutator == null) {
return false;
}
JacksonInject.Value injectable = ctxt.getAnnotationIntrospector()
.findInjectableValue(ctxt.getConfig(), mutator);
if ((injectable == null) || !Boolean.FALSE.equals(injectable.getUseInput())) {
return false;
}
// Only when the member really is the one injection uses: [databind#4218] drops
// injectables masked by a Creator parameter with the same id, and dropping the
// mutator for those would leave the property unset instead of injected.
Map<Object, AnnotatedMember> injectables = beanDesc.findInjectables();
return (injectables != null) && injectables.containsValue(mutator);
}

/**
* Method that will find if bean has any managed- or back-reference properties,
* and if so add them to bean, to be linked during resolution phase.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package tools.jackson.databind.deser.inject;

import java.util.LinkedHashMap;
import java.util.Map;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.*;

import tools.jackson.databind.InjectableValues;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.annotation.JsonDeserialize;
import tools.jackson.databind.annotation.JsonPOJOBuilder;
import tools.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.assertEquals;

// [databind#6201]: `useInput = OptBoolean.FALSE` must drop value from input for
// Field- and Setter-backed properties too, not just Creator ones
class JacksonInject6201Test extends DatabindTestUtil
{
static class FieldBean {
@JacksonInject(value = "tenant", useInput = OptBoolean.FALSE)
public String tenant = "unset";

public String title = "";
}

static class SetterBean {
String tenant = "unset";
public String title = "";

@JacksonInject(value = "tenant", useInput = OptBoolean.FALSE)
public void setTenant(String t) { tenant = t; }
}

@JsonFormat(shape = JsonFormat.Shape.ARRAY)
@JsonPropertyOrder({ "title", "tenant" })
static class AsArrayBean {
public String title = "";

@JacksonInject(value = "tenant", useInput = OptBoolean.FALSE)
public String tenant = "unset";
}

@JsonDeserialize(builder = BuiltBean.Builder.class)
static class BuiltBean {
public final String tenant, title;

BuiltBean(String tenant, String title) {
this.tenant = tenant;
this.title = title;
}

@JsonPOJOBuilder(withPrefix = "set")
static class Builder {
@JacksonInject(value = "tenant", useInput = OptBoolean.FALSE)
public String tenant = "unset";

String title = "";

public Builder setTitle(String t) { title = t; return this; }

public BuiltBean build() { return new BuiltBean(tenant, title); }
}
}

static class UnwrappedBean {
@JsonUnwrapped
public FieldBean inner = new FieldBean();
}

static class AnySetterBean {
@JacksonInject(value = "tenant", useInput = OptBoolean.FALSE)
public String tenant = "unset";

public Map<String, Object> leftovers = new LinkedHashMap<>();

@JsonAnySetter
public void addLeftover(String name, Object value) { leftovers.put(name, value); }
}

static class UseInputTrueBean {
@JacksonInject(value = "tenant", useInput = OptBoolean.TRUE)
public String tenant = "unset";
}

static class UseInputDefaultBean {
@JacksonInject("tenant")
public String tenant = "unset";
}

private final ObjectMapper MAPPER = jsonMapperBuilder()
.injectableValues(new InjectableValues.Std().addValue("tenant", "injected"))
.build();

private final String DOC = """
{"tenant":"from-input","title":"x"}
""";

@Test
void injectOnlyField() throws Exception {
assertEquals("injected", MAPPER.readValue(DOC, FieldBean.class).tenant);
}

@Test
void injectOnlySetter() throws Exception {
assertEquals("injected", MAPPER.readValue(DOC, SetterBean.class).tenant);
}

@Test
void injectOnlyAsArray() throws Exception {
AsArrayBean bean = MAPPER.readValue("""
["x","from-input"]
""", AsArrayBean.class);
assertEquals("injected", bean.tenant);
assertEquals("x", bean.title);
}

@Test
void injectOnlyWithBuilder() throws Exception {
assertEquals("injected", MAPPER.readValue(DOC, BuiltBean.class).tenant);
}

@Test
void injectOnlyWhenUnwrapped() throws Exception {
assertEquals("injected", MAPPER.readValue(DOC, UnwrappedBean.class).inner.tenant);
}

// Value from input must not reach the any-setter either
@Test
void injectOnlyWithAnySetter() throws Exception {
AnySetterBean bean = MAPPER.readValue("""
{"tenant":"from-input"}
""", AnySetterBean.class);
assertEquals("injected", bean.tenant);
assertEquals(0, bean.leftovers.size());
}

@Test
void injectOnlyWhenUpdating() throws Exception {
FieldBean bean = MAPPER.readerForUpdating(new FieldBean()).readValue(DOC);
assertEquals("injected", bean.tenant);
}

// ... while the other two settings keep binding from input
@Test
void useInputTrueStillBinds() throws Exception {
assertEquals("from-input", MAPPER.readValue(DOC, UseInputTrueBean.class).tenant);
}

@Test
void useInputDefaultStillBinds() throws Exception {
assertEquals("from-input", MAPPER.readValue(DOC, UseInputDefaultBean.class).tenant);
}
}
Loading