-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(#1381): Add a way to specify "inject-only" with @JacksonInject #5175
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
base: 2.x
Are you sure you want to change the base?
Changes from 9 commits
9a42cca
d714aeb
d6bee5f
bc094d8
a12a3d7
69c603e
191292c
5705aff
bc9d37e
272c499
5da46ac
deb1741
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -817,11 +817,23 @@ protected void addInjectables(DeserializationContext ctxt, | |
for (Map.Entry<Object, AnnotatedMember> entry : raw.entrySet()) { | ||
AnnotatedMember m = entry.getValue(); | ||
final JacksonInject.Value injectableValue = introspector.findInjectableValue(m); | ||
final Boolean optional = injectableValue == null ? null : injectableValue.getOptional(); | ||
final Boolean optional; | ||
final Boolean useInput; | ||
|
||
builder.addInjectable(PropertyName.construct(m.getName()), | ||
m.getType(), | ||
beanDesc.getClassAnnotations(), m, entry.getKey(), optional); | ||
if (injectableValue == null) { | ||
optional = null; | ||
useInput = null; | ||
} else { | ||
optional = injectableValue.getOptional(); | ||
useInput = injectableValue.getUseInput(); | ||
} | ||
|
||
// 04-Jun-2025, tatu: [databind#1381]: default for "useInput" is false | ||
if (!Boolean.TRUE.equals(useInput)) { | ||
builder.addInjectable(PropertyName.construct(m.getName()), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is part I don't fully understand: if injectable is only added if no input is to be used, how do things work with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As per this test, with no input but injected value present, the injected value is always returned regardless of @Test
@DisplayName("input NO, injectable YES, useInput DEFAULT|TRUE|FALSE => injected")
void test2() throws Exception {
assertEquals("injected", injectedMapper.readValue(empty, InputDefault.class).getField());
assertEquals("injected", injectedMapper.readValue(empty, InputTrue.class).getField());
assertEquals("injected", injectedMapper.readValue(empty, InputFalse.class).getField());
} I read it like "we have no input but we have a value to inject, so we don't care about anything else: let's use the injected value". Is this the expected behavior? Or you meant something else? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logic-wise, yes, you are correct (expected behavior I agree with). But I meant code; meaning... what does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a way to eagerly discard the injectable if we're going to use the input anyways. I think it's better in terms of performance since any further evaluation is useless, and there are many methods involved down that path.
Not now maybe, but in general I get that if a field is marked as injected, we should add it to the internal injectables for whatever future logic. I can try to change this, which means passing builder.addInjectable(PropertyName.construct(m.getName()),
m.getType(),
beanDesc.getClassAnnotations(), m, entry.getKey(), optional, useInput); Maybe we could also think of passing the whole But I think this is not going to be easy. What do you think, is it worth it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, figured it was meant as optimization. So we cannot know statically which one to use. And yes, I think this is necessary to handle properly. But I understand things are messy, tricky, complicated, esp. when passing things via Constructors (CreatorProperty properties) vs Field/MethodProperty ones. |
||
m.getType(), | ||
beanDesc.getClassAnnotations(), m, entry.getKey(), optional); | ||
} | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
import java.io.IOException; | ||
import java.util.BitSet; | ||
|
||
import com.fasterxml.jackson.annotation.JacksonInject; | ||
import com.fasterxml.jackson.core.JsonParser; | ||
import com.fasterxml.jackson.databind.*; | ||
import com.fasterxml.jackson.databind.deser.SettableAnyProperty; | ||
|
@@ -218,6 +219,25 @@ public Object[] getParameters(SettableBeanProperty[] props) | |
if (_anyParamSetter != null) { | ||
_creatorParameters[_anyParamSetter.getParameterIndex()] = _createAndSetAnySetterValue(); | ||
} | ||
|
||
for (int ix = 0; ix < props.length; ++ix) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the moment when creator properties are resolved, so I think it's ok to have the logic here, as per the method's javadoc as well:
I placed the new part right before the code that checks for null creator properties in case we have an injected value to use, so to not fail. Should I move it after? |
||
final SettableBeanProperty prop = props[ix]; | ||
final AnnotatedMember member = prop.getMember(); | ||
|
||
if (member != null) { | ||
final JacksonInject.Value injectableValue = prop.getInjectableValue(); | ||
|
||
if (injectableValue != null && !Boolean.TRUE.equals(injectableValue.getUseInput())) { | ||
final Object injectedValue = _context.findInjectableValue( | ||
injectableValue.getId(), prop, member, injectableValue.getOptional()); | ||
|
||
if (injectedValue != JacksonInject.Value.empty()) { | ||
_creatorParameters[ix] = injectedValue; | ||
} | ||
} | ||
} | ||
} | ||
|
||
if (_context.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) { | ||
for (int ix = 0; ix < props.length; ++ix) { | ||
if (_creatorParameters[ix] == null) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
package com.fasterxml.jackson.databind.deser.inject; | ||
|
||
import com.fasterxml.jackson.annotation.JacksonInject; | ||
import com.fasterxml.jackson.annotation.JsonCreator; | ||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import com.fasterxml.jackson.annotation.OptBoolean; | ||
import com.fasterxml.jackson.databind.DeserializationFeature; | ||
import com.fasterxml.jackson.databind.InjectableValues; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.fasterxml.jackson.databind.exc.ValueInstantiationException; | ||
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||
|
||
class JacksonInject1381DeserializationFeatureDisabledTest extends DatabindTestUtil { | ||
static class InputDefault { | ||
@JacksonInject(value = "key") | ||
@JsonProperty("field") | ||
private final String _field; | ||
|
||
@JsonCreator | ||
public InputDefault(@JsonProperty("field") final String field) { | ||
_field = field; | ||
} | ||
|
||
public String getField() { | ||
return _field; | ||
} | ||
} | ||
|
||
static class InputDefaultConstructor { | ||
private final String _field; | ||
|
||
@JsonCreator | ||
public InputDefaultConstructor(@JacksonInject(value = "key") | ||
@JsonProperty("field") final String field) { | ||
_field = field; | ||
} | ||
|
||
public String getField() { | ||
return _field; | ||
} | ||
} | ||
|
||
static class InputTrue { | ||
@JacksonInject(value = "key", useInput = OptBoolean.TRUE) | ||
@JsonProperty("field") | ||
private final String _field; | ||
|
||
@JsonCreator | ||
public InputTrue(@JsonProperty("field") final String field) { | ||
_field = field; | ||
} | ||
|
||
public String getField() { | ||
return _field; | ||
} | ||
} | ||
|
||
static class InputTrueConstructor { | ||
private final String _field; | ||
|
||
@JsonCreator | ||
public InputTrueConstructor(@JacksonInject(value = "key", useInput = OptBoolean.TRUE) | ||
@JsonProperty("field") final String field) { | ||
_field = field; | ||
} | ||
|
||
public String getField() { | ||
return _field; | ||
} | ||
|
||
} | ||
|
||
static class InputFalse { | ||
@JacksonInject(value = "key", useInput = OptBoolean.FALSE) | ||
@JsonProperty("field") | ||
private final String _field; | ||
|
||
@JsonCreator | ||
public InputFalse(@JsonProperty("field") final String field) { | ||
_field = field; | ||
} | ||
|
||
public String getField() { | ||
return _field; | ||
} | ||
} | ||
|
||
static class InputFalseConstructor { | ||
private final String _field; | ||
|
||
@JsonCreator | ||
public InputFalseConstructor(@JacksonInject(value = "key", useInput = OptBoolean.FALSE) | ||
@JsonProperty("field") final String field) { | ||
_field = field; | ||
} | ||
|
||
public String getField() { | ||
return _field; | ||
} | ||
} | ||
|
||
private final String empty = "{}"; | ||
private final String input = "{\"field\": \"input\"}"; | ||
|
||
private final ObjectMapper plainMapper = newJsonMapper() | ||
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE); | ||
private final ObjectMapper injectedMapper = jsonMapperBuilder() | ||
.injectableValues(new InjectableValues.Std().addValue("key", "injected")) | ||
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE) | ||
.build(); | ||
|
||
@Test | ||
@DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input NO, injectable NO, useInput DEFAULT|TRUE|FALSE => exception") | ||
void test1() { | ||
assertThrows(ValueInstantiationException.class, | ||
() -> plainMapper.readValue(empty, InputDefault.class)); | ||
assertThrows(ValueInstantiationException.class, | ||
() -> plainMapper.readValue(empty, InputDefaultConstructor.class)); | ||
|
||
assertThrows(ValueInstantiationException.class, | ||
() -> plainMapper.readValue(empty, InputTrue.class)); | ||
assertThrows(ValueInstantiationException.class, | ||
() -> plainMapper.readValue(empty, InputTrueConstructor.class)); | ||
|
||
assertThrows(ValueInstantiationException.class, | ||
() -> plainMapper.readValue(empty, InputFalse.class)); | ||
assertThrows(ValueInstantiationException.class, | ||
() -> plainMapper.readValue(empty, InputFalseConstructor.class)); | ||
} | ||
|
||
@Test | ||
@DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input NO, injectable YES, useInput DEFAULT|TRUE|FALSE => injected") | ||
void test2() throws Exception { | ||
assertEquals("injected", injectedMapper.readValue(empty, InputDefault.class).getField()); | ||
assertEquals("injected", injectedMapper.readValue(empty, InputDefaultConstructor.class).getField()); | ||
assertEquals("injected", injectedMapper.readValue(empty, InputTrue.class).getField()); | ||
assertEquals("injected", injectedMapper.readValue(empty, InputTrueConstructor.class).getField()); | ||
assertEquals("injected", injectedMapper.readValue(empty, InputFalse.class).getField()); | ||
assertEquals("injected", injectedMapper.readValue(empty, InputFalseConstructor.class).getField()); | ||
} | ||
|
||
@Test | ||
@DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input YES, injectable NO, useInput DEFAULT|FALSE => exception") | ||
void test3() throws Exception { | ||
assertEquals("input", plainMapper.readValue(input, InputDefault.class).getField()); | ||
assertEquals("input", plainMapper.readValue(input, InputDefaultConstructor.class).getField()); | ||
assertEquals("input", plainMapper.readValue(input, InputFalse.class).getField()); | ||
assertEquals("input", plainMapper.readValue(input, InputFalseConstructor.class).getField()); | ||
} | ||
|
||
@Test | ||
@DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input YES, injectable NO, useInput TRUE => input") | ||
void test4() throws Exception { | ||
assertEquals("input", plainMapper.readValue(input, InputTrue.class).getField()); | ||
assertEquals("input", plainMapper.readValue(input, InputTrueConstructor.class).getField()); | ||
} | ||
|
||
@Test | ||
@DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input YES, injectable YES, useInput DEFAULT|FALSE => injected") | ||
void test5() throws Exception { | ||
assertEquals("injected", injectedMapper.readValue(input, InputDefault.class).getField()); | ||
assertEquals("injected", injectedMapper.readValue(input, InputDefaultConstructor.class).getField()); | ||
assertEquals("injected", injectedMapper.readValue(input, InputFalse.class).getField()); | ||
assertEquals("injected", injectedMapper.readValue(input, InputFalseConstructor.class).getField()); | ||
} | ||
|
||
@Test | ||
@DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input YES, injectable YES, useInput TRUE => input") | ||
void test6() throws Exception { | ||
assertEquals("input", injectedMapper.readValue(input, InputTrue.class).getField()); | ||
assertEquals("input", injectedMapper.readValue(input, InputTrueConstructor.class).getField()); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.