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 feb59ae24f..056483f114 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) #5045: If there is a no-parameter constructor marked as `JsonCreator` and a constructor reported as `DefaultCreator`, latter is incorrectly used (reported by @wrongwrong) diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java index 53b494f100..bfd3db28d6 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/CreatorProperty.java b/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java index e6d7df4b18..9ce62e6472 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java @@ -280,6 +280,11 @@ public Object getInjectableValueId() { return (_injectableValue == null) ? null : _injectableValue.getId(); } + @Override // since 2.21 + public JacksonInject.Value getInjectionDefinition() { + return _injectableValue; + } + @Override public boolean isInjectionOnly() { return (_injectableValue != null) && !_injectableValue.willUseInput(true); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/SettableBeanProperty.java b/src/main/java/com/fasterxml/jackson/databind/deser/SettableBeanProperty.java index bbc05bc2d0..f08a481379 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/SettableBeanProperty.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/SettableBeanProperty.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.lang.annotation.Annotation; +import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.deser.impl.FailingDeserializer; @@ -468,6 +469,14 @@ public int getCreatorIndex() { */ public Object getInjectableValueId() { return null; } + /** + * Accessor for injection definition, if this bean property supports + * value injection. + * + * @since 2.21 + */ + public JacksonInject.Value getInjectionDefinition() { return null; } + /** * Accessor for checking whether this property is injectable, and if so, * ONLY injectable (will not bind from input). 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 db68167e5a..f16796931f 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.*; @@ -89,6 +90,22 @@ public class PropertyValueBuffer */ protected PropertyValue _anyParamBuffered; + /** + * Bitflag used to track already injected parameters when number of parameters is + * less than 32 (fits in int). + * + * @since 2.21 + */ + protected int _paramsInjected; + + /** + * Bitflag used to track already injected parameters when number of parameters is + * 32 or higher. + * + * @since 2.21 + */ + protected final BitSet _paramsInjectedBig; + /* /********************************************************** /* Life-cycle @@ -108,8 +125,10 @@ public PropertyValueBuffer(JsonParser p, DeserializationContext ctxt, int paramC _creatorParameters = new Object[paramCount]; if (paramCount < 32) { _paramsSeenBig = null; + _paramsInjectedBig = null; } else { _paramsSeenBig = new BitSet(); + _paramsInjectedBig = new BitSet(); } // Only care about Creator-bound Any setters: if ((anyParamSetter == null) || (anyParamSetter.getParameterIndex() < 0)) { @@ -218,6 +237,7 @@ public Object[] getParameters(SettableBeanProperty[] props) if (_anyParamSetter != null) { _creatorParameters[_anyParamSetter.getParameterIndex()] = _createAndSetAnySetterValue(); } + if (_context.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) { for (int ix = 0; ix < props.length; ++ix) { if (_creatorParameters[ix] == null) { @@ -228,6 +248,19 @@ public Object[] getParameters(SettableBeanProperty[] props) } } } + + if (_paramsInjectedBig == null) { + for (int ix = 0; ix < _creatorParameters.length; ++ix) { + if ((_paramsInjected & 1) == 0) { + _inject(props[ix]); + } + } + } else { + for (int ix = 0; (ix = _paramsInjectedBig.nextClearBit(ix)) < _creatorParameters.length; ++ix) { + _inject(props[ix]); + } + } + return _creatorParameters; } @@ -268,6 +301,7 @@ protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingExcep // First: do we have injectable value? Object injectableValueId = prop.getInjectableValueId(); if (injectableValueId != null) { + _trackInjected(prop); return _context.findInjectableValue(prop.getInjectableValueId(), prop, null, null, null); } @@ -302,6 +336,32 @@ protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingExcep } } + 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()) { + _trackInjected(prop); + _creatorParameters[prop.getCreatorIndex()] = value; + } + } + } + } + + private void _trackInjected(final SettableBeanProperty prop) { + if (_paramsInjectedBig == null) { + _paramsInjected |= 1 << prop.getCreatorIndex(); + } else { + _paramsInjectedBig.set(prop.getCreatorIndex()); + } + } + /* /********************************************************** /* Other methods 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);