diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 9647c782d9..fbe2b55b10 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -1966,6 +1966,8 @@ Giulio Longfils (@giulong) * Contributed #4218: If `@JacksonInject` is specified for field and deserialized by the Creator, the inject process will be executed twice (2.20.0) + * Contributed #1381: Add a way to specify "inject-only" with `@JacksonInject` + (2.21.0) Plamen Tanov (@ptanov) * Reported #2678: `@JacksonInject` added to property overrides value from the JSON diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index f835da2ffb..1840f58a63 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -6,6 +6,8 @@ Project: jackson-databind 2.21.0 (not yet released) +#1381: Add a way to specify "inject-only" with `@JacksonInject` + (fix by Giulio L) #1547: Un-deprecate `SerializationFeature.WRITE_EMPTY_JSON_ARRAYS` #5045: If there is a no-parameter constructor marked as `JsonCreator` and a constructor reported as `DefaultCreator`, latter is incorrectly used diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java index 53b494f100..39e359aaa6 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java @@ -469,10 +469,12 @@ public final Object findInjectableValue(Object valueId, throws JsonMappingException { if (_injectableValues == null) { - // `optional` comes from property annotation (if any); has precedence - // over global setting. - if (Boolean.TRUE.equals(optional) - || (optional == null && !isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE))) { + // `useInput` and `optional` come from property annotation (if any); + // they have precedence over global setting. + if (Boolean.TRUE.equals(useInput) + || Boolean.TRUE.equals(optional) + || ((useInput == null || optional == null) + && !isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE))) { return JacksonInject.Value.empty(); } throw missingInjectableValueException(String.format( diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java index 84872e26d0..0c020ad830 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java @@ -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.*; @@ -90,7 +91,8 @@ public class PropertyValueBuffer protected PropertyValue _anyParamBuffered; /** - * Indexes properties that are injectable, if any; {@code null} if none. + * Indexes properties that are injectable, if any; {@code null} if none, + * cleared as they are injected. * * @since 2.21 */ @@ -229,8 +231,19 @@ public Object[] getParameters(SettableBeanProperty[] props) if (_anyParamSetter != null) { _creatorParameters[_anyParamSetter.getParameterIndex()] = _createAndSetAnySetterValue(); } + + // [databind#1381] handle inject-only (useInput = false) properties + if (_injectablePropIndexes != null) { + int ix = _injectablePropIndexes.nextSetBit(0); + while (ix >= 0) { + _inject(props[ix]); + ix = _injectablePropIndexes.nextSetBit(ix + 1); + } + } + if (_context.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) { - for (int ix = 0; ix < props.length; ++ix) { + final int len = _creatorParameters.length; + for (int ix = 0; ix < len; ++ix) { if (_creatorParameters[ix] == null) { SettableBeanProperty prop = props[ix]; _context.reportInputMismatch(prop, @@ -239,6 +252,7 @@ public Object[] getParameters(SettableBeanProperty[] props) } } } + return _creatorParameters; } @@ -279,6 +293,8 @@ protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingExcep // First: do we have injectable value? Object injectableValueId = prop.getInjectableValueId(); if (injectableValueId != null) { + // 10-Nov-2025: [databind#1381] Is this needed? + _injectablePropIndexes.clear(prop.getCreatorIndex()); return _context.findInjectableValue(prop.getInjectableValueId(), prop, null, null, null); } @@ -313,6 +329,30 @@ protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingExcep } } + /** + * Method called to inject value for given property, possibly overriding + * assigned (from input) value. + * + * @since 2.21 + */ + private void _inject(final SettableBeanProperty prop) throws JsonMappingException { + final JacksonInject.Value injection = prop.getInjectionDefinition(); + + if (injection != null) { + final Boolean useInput = injection.getUseInput(); + + if (!Boolean.TRUE.equals(useInput)) { + final Object value = _context.findInjectableValue(injection.getId(), + prop, prop.getMember(), injection.getOptional(), useInput); + + if (value != JacksonInject.Value.empty()) { + int ix = prop.getCreatorIndex(); + _creatorParameters[ix] = value; + } + } + } + } + /* /********************************************************** /* Other methods @@ -392,6 +432,7 @@ public boolean assignParameter(SettableBeanProperty prop, Object value) _paramsSeenBig.set(ix); if (--_paramsNeeded <= 0) { // 29-Nov-2016, tatu: But! May still require Object Id value + return (_objectIdReader == null) || (_idValue != null); } } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/ValueInjector.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/ValueInjector.java index 0e6c0c7d9b..2a6b3bd7c2 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/ValueInjector.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/ValueInjector.java @@ -69,7 +69,15 @@ public void inject(DeserializationContext context, Object beanInstance) throws IOException { final Object value = findValue(context, beanInstance); - if (!JacksonInject.Value.empty().equals(value)) { + + if (value == JacksonInject.Value.empty()) { + if (Boolean.FALSE.equals(_optional)) { + throw context.missingInjectableValueException( + String.format("No injectable value with id '%s' found (for property '%s')", + _valueId, getName()), + _valueId, null, beanInstance); + } + } else if (!Boolean.TRUE.equals(_useInput)) { _member.setValue(beanInstance, value); } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381DeserializationFeatureDisabledTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381DeserializationFeatureDisabledTest.java new file mode 100644 index 0000000000..ca0ea33a4f --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381DeserializationFeatureDisabledTest.java @@ -0,0 +1,179 @@ +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 = jsonMapperBuilder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE) + .build(); + 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()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java new file mode 100644 index 0000000000..1473d3516d --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381Test.java @@ -0,0 +1,189 @@ +package com.fasterxml.jackson.databind.deser.inject; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +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.InjectableValues; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.MissingInjectableValueExcepion; +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class JacksonInject1381Test 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(); + private final ObjectMapper injectedMapper = jsonMapperBuilder() + .injectableValues(new InjectableValues.Std().addValue("key", "injected")) + .build(); + + @Test + @DisplayName("input NO, injectable NO, useInput DEFAULT|TRUE|FALSE => exception") + void test1() { + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputDefault.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputDefaultConstructor.class)); + + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputTrue.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputTrueConstructor.class)); + + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputFalse.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputFalseConstructor.class)); + } + + @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, 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("input YES, injectable NO, useInput DEFAULT|FALSE => exception") + void test3() { + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(input, InputDefault.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(input, InputDefaultConstructor.class)); + + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(input, InputFalse.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(input, InputFalseConstructor.class)); + } + + @Test + @DisplayName("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("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("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()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalDeserializationFeatureDisabledTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalDeserializationFeatureDisabledTest.java new file mode 100644 index 0000000000..bfc88d184f --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalDeserializationFeatureDisabledTest.java @@ -0,0 +1,181 @@ +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 JacksonInject1381WithOptionalDeserializationFeatureDisabledTest extends DatabindTestUtil +{ + static class InputDefault + { + @JacksonInject(value = "key", optional = OptBoolean.TRUE) + @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", optional = OptBoolean.TRUE) + @JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + static class InputTrue + { + @JacksonInject(value = "key", useInput = OptBoolean.TRUE, optional = 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, optional = OptBoolean.TRUE) + @JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + + } + + static class InputFalse + { + @JacksonInject(value = "key", useInput = OptBoolean.FALSE, optional = OptBoolean.TRUE) + @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, optional = OptBoolean.TRUE) + @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 = jsonMapperBuilder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE) + .build(); + 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, optional YES, 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, optional YES, 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, optional YES, input YES, injectable NO, useInput DEFAULT|TRUE|FALSE => input") + 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()); + assertEquals("input", plainMapper.readValue(input, InputTrue.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputTrueConstructor.class).getField()); + } + + @Test + @DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, optional YES, input YES, injectable YES, useInput DEFAULT|FALSE => injected") + void test4() 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, optional YES, input YES, injectable YES, useInput TRUE => input") + void test5() throws Exception { + assertEquals("input", injectedMapper.readValue(input, InputTrue.class).getField()); + assertEquals("input", injectedMapper.readValue(input, InputTrueConstructor.class).getField()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java new file mode 100644 index 0000000000..990d6a501e --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalTest.java @@ -0,0 +1,177 @@ +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.InjectableValues; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.MissingInjectableValueExcepion; +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 JacksonInject1381WithOptionalTest extends DatabindTestUtil +{ + static class InputDefault + { + @JacksonInject(value = "key", optional = OptBoolean.TRUE) + @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", optional = OptBoolean.TRUE) + @JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + } + + static class InputTrue + { + @JacksonInject(value = "key", useInput = OptBoolean.TRUE, optional = 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, optional = OptBoolean.TRUE) + @JsonProperty("field") final String field) { + _field = field; + } + + public String getField() { + return _field; + } + + } + + static class InputFalse + { + @JacksonInject(value = "key", useInput = OptBoolean.FALSE, optional = OptBoolean.TRUE) + @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, optional = OptBoolean.TRUE) + @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(); + private final ObjectMapper injectedMapper = jsonMapperBuilder() + .injectableValues(new InjectableValues.Std().addValue("key", "injected")) + .build(); + + @Test + @DisplayName("optional YES, input NO, injectable NO, useInput DEFAULT|TRUE|FALSE => exception") + void test1() { + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputDefault.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputDefaultConstructor.class)); + + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputTrue.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputTrueConstructor.class)); + + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputFalse.class)); + assertThrows(MissingInjectableValueExcepion.class, + () -> plainMapper.readValue(empty, InputFalseConstructor.class)); + } + + @Test + @DisplayName("optional YES, 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("optional YES, input YES, injectable NO, useInput DEFAULT|TRUE|FALSE => input") + 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()); + assertEquals("input", plainMapper.readValue(input, InputTrue.class).getField()); + assertEquals("input", plainMapper.readValue(input, InputTrueConstructor.class).getField()); + } + + @Test + @DisplayName("optional YES, input YES, injectable YES, useInput DEFAULT|FALSE => injected") + void test4() 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("optional YES, input YES, injectable YES, useInput TRUE => input") + void test5() throws Exception { + assertEquals("input", injectedMapper.readValue(input, InputTrue.class).getField()); + assertEquals("input", injectedMapper.readValue(input, InputTrueConstructor.class).getField()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject2678Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject2678Test.java index 4d352724b9..6de48d1ed0 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject2678Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject2678Test.java @@ -55,8 +55,6 @@ void readValueInjectables() throws Exception { final Some actualValuePresent = mapper.readValue( "{\"field1\": \"field1value\", \"field2\": \"field2value\"}", Some.class); assertEquals("field1value", actualValuePresent.getField1()); - - // if I comment @JacksonInject that is next to the property the valid assert is the correct one: assertEquals("field2value", actualValuePresent.getField2()); } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject3072Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject3072Test.java index 0264d97a9d..cf2b0c0301 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject3072Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject3072Test.java @@ -22,14 +22,6 @@ static class DtoWithOptional { @JacksonInject(value = "optionalField", optional = OptBoolean.TRUE) String optionalField; - - public String getId() { - return id; - } - - public String getOptionalField() { - return optionalField; - } } static class DtoWithRequired { @@ -84,7 +76,7 @@ void testRequiredAnnotatedField() throws Exception { MissingInjectableValueExcepion.class, () -> reader.readValue("{}")); assertThat(exception.getMessage()) - .startsWith("No 'injectableValues' configured, cannot inject value with id 'requiredValue'"); + .startsWith("No injectable value with id 'requiredValue' found (for property 'requiredField')"); // Also check the other code path, with non-null Injectables ObjectReader reader2 = reader.with(new InjectableValues.Std() diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/inject/TestInjectables.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/TestInjectables.java index 52199aa2f2..47cfb5be5b 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/inject/TestInjectables.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/TestInjectables.java @@ -5,13 +5,11 @@ import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; import static org.junit.jupiter.api.Assertions.*; -import static com.fasterxml.jackson.databind.testutil.DatabindTestUtil.a2q; -import static com.fasterxml.jackson.databind.testutil.DatabindTestUtil.newJsonMapper; - -public class TestInjectables +public class TestInjectables extends DatabindTestUtil { static class InjectedBean { @@ -104,12 +102,13 @@ public void setMethodValue(String methodValue) { @Test public void testSimple() throws Exception { - ObjectMapper mapper = newJsonMapper(); - mapper.setInjectableValues(new InjectableValues.Std() + ObjectMapper mapper = jsonMapperBuilder() + .injectableValues(new InjectableValues.Std() .addValue(String.class, "stuffValue") .addValue("myId", "xyz") .addValue(Long.TYPE, Long.valueOf(37)) - ); + ) + .build(); InjectedBean bean = mapper.readValue("{\"value\":3}", InjectedBean.class); assertEquals(3, bean.value); assertEquals("stuffValue", bean.stuff);