diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index ae59365989..b0cc5200e7 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -1945,6 +1945,17 @@ Ryan Schmitt (@rschmitt) (2.19.0) Giulio Longfils (@giulong) + * Contributed #1381: Add a way to specify "inject-only" with `@JacksonInject` + (2.20.0) + * Contributed fix for #2678: `@JacksonInject` added to property overrides value + from the JSON even if `useInput` is `OptBoolean.TRUE` + (2.20.0) * Contributed #3072: Allow specifying `@JacksonInject` does not fail when there's no corresponding value (2.20.0) + +Plamen Tanov (@ptanov) + * Reported #2678: `@JacksonInject` added to property overrides value from the JSON even if + `useInput` is `OptBoolean.TRUE` + (2.20.0) + diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 1930be59e8..778a9718fa 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -6,6 +6,12 @@ Project: jackson-databind 2.20.0 (not yet released) +#1381: Add a way to specify "inject-only" with `@JacksonInject` + (contributed by Giulio L) +#2678: `@JacksonInject` added to property overrides value from the JSON even if + `useInput` is `OptBoolean.TRUE` + (reported by Plamen T) + (fix contributed by Giulio L) #3072: Allow specifying `@JacksonInject` does not fail when there's no corresponding value (requested by Lavender S) diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java index b18244bec3..06037fa79e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java @@ -465,21 +465,24 @@ public final boolean hasSomeOfFeatures(int featureMask) { * @since 2.20 */ public final Object findInjectableValue(Object valueId, - BeanProperty forProperty, Object beanInstance, Boolean optional) + BeanProperty forProperty, Object beanInstance, Boolean optional, Boolean useInput) 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))) { + 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( "No 'injectableValues' configured, cannot inject value with id '%s'", valueId), valueId, forProperty, beanInstance); } - return _injectableValues.findInjectableValue(this, valueId, forProperty, beanInstance, optional); + return _injectableValues.findInjectableValue(this, valueId, forProperty, beanInstance, + optional, useInput); } /** @@ -490,7 +493,7 @@ public final Object findInjectableValue(Object valueId, BeanProperty forProperty, Object beanInstance) throws JsonMappingException { - return findInjectableValue(valueId, forProperty, beanInstance, null); + return findInjectableValue(valueId, forProperty, beanInstance, null, null); } /** diff --git a/src/main/java/com/fasterxml/jackson/databind/InjectableValues.java b/src/main/java/com/fasterxml/jackson/databind/InjectableValues.java index 9950570696..0536e29e31 100644 --- a/src/main/java/com/fasterxml/jackson/databind/InjectableValues.java +++ b/src/main/java/com/fasterxml/jackson/databind/InjectableValues.java @@ -28,7 +28,7 @@ public abstract class InjectableValues */ public Object findInjectableValue(DeserializationContext ctxt, Object valueId, BeanProperty forProperty, Object beanInstance, - Boolean optional) + Boolean optional, Boolean useInput) throws JsonMappingException { // For backwards-compatibility, must delegate to old method @@ -83,7 +83,7 @@ public Std addValue(Class classKey, Object value) { */ @Override public Object findInjectableValue(DeserializationContext ctxt, Object valueId, - BeanProperty forProperty, Object beanInstance, Boolean optional) + BeanProperty forProperty, Object beanInstance, Boolean optional, Boolean useInput) throws JsonMappingException { if (!(valueId instanceof String)) { @@ -116,7 +116,7 @@ public Object findInjectableValue(DeserializationContext ctxt, Object valueId, public Object findInjectableValue(Object valueId, DeserializationContext ctxt, BeanProperty forProperty, Object beanInstance) throws JsonMappingException { - return this.findInjectableValue(ctxt, valueId, forProperty, beanInstance, null); + return this.findInjectableValue(ctxt, valueId, forProperty, beanInstance, null, null); } } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBuilder.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBuilder.java index 6888cb57ec..5d935b3c0a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBuilder.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBuilder.java @@ -244,7 +244,7 @@ public void addBackReferenceProperty(String referenceName, SettableBeanProperty */ public void addInjectable(PropertyName propName, JavaType propType, Annotations contextAnnotations, AnnotatedMember member, - Object valueId, Boolean optional) + Object valueId, Boolean optional, Boolean useInput) throws JsonMappingException { if (_injectables == null) { @@ -257,7 +257,7 @@ public void addInjectable(PropertyName propName, JavaType propType, _handleBadAccess(e); } } - _injectables.add(new ValueInjector(propName, propType, member, valueId, optional)); + _injectables.add(new ValueInjector(propName, propType, member, valueId, optional, useInput)); } /** @@ -269,7 +269,7 @@ public void addInjectable(PropertyName propName, JavaType propType, Object valueId) throws JsonMappingException { - this.addInjectable(propName, propType, contextAnnotations, member, valueId, null); + this.addInjectable(propName, propType, contextAnnotations, member, valueId, null, null); } /** diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java index bf74f6e317..13865b754c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java @@ -822,11 +822,11 @@ protected void addInjectables(DeserializationContext ctxt, for (Map.Entry entry : raw.entrySet()) { AnnotatedMember m = entry.getValue(); final JacksonInject.Value injectableValue = introspector.findInjectableValue(m); - final Boolean optional = injectableValue == null ? null : injectableValue.getOptional(); builder.addInjectable(PropertyName.construct(m.getName()), m.getType(), - beanDesc.getClassAnnotations(), m, entry.getKey(), optional); + beanDesc.getClassAnnotations(), m, entry.getKey(), + injectableValue.getOptional(), injectableValue.getUseInput()); } } } 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..c5a9df0395 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 + public JacksonInject.Value getInjectableValue() { + 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 5c02e63bb2..baee3fce90 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; @@ -458,6 +459,14 @@ public int getCreatorIndex() { */ public Object getInjectableValueId() { return null; } + /** + * Accessor for injectable value, if this bean property supports + * value injection. + * + * @since 2.20 + */ + public JacksonInject.Value getInjectableValue() { 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 a3c4e81d39..7d5d894f44 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.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) { + final SettableBeanProperty prop = props[ix]; + final JacksonInject.Value injectableValue = prop.getInjectableValue(); + + if (injectableValue != null) { + final Boolean useInput = injectableValue.getUseInput(); + + if (!Boolean.TRUE.equals(useInput)) { + final Object value = _context.findInjectableValue(injectableValue.getId(), + prop, prop.getMember(), injectableValue.getOptional(), useInput); + + if (value != JacksonInject.Value.empty()) { + _creatorParameters[ix] = value; + } + } + } + } + if (_context.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) { for (int ix = 0; ix < props.length; ++ix) { if (_creatorParameters[ix] == null) { @@ -269,7 +289,7 @@ protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingExcep Object injectableValueId = prop.getInjectableValueId(); if (injectableValueId != null) { return _context.findInjectableValue(prop.getInjectableValueId(), - prop, null, null); + prop, null, null, null); } // Second: required? if (prop.isRequired()) { 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 c6c4b2aad0..d8c6d057c7 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 @@ -29,15 +29,24 @@ public class ValueInjector */ protected final Boolean _optional; + /** + * Flag used for configuring the behavior when the input value should be preferred + * over the value to inject. + * + * @since 2.20 + */ + protected final Boolean _useInput; + /** * @since 2.20 */ public ValueInjector(PropertyName propName, JavaType type, - AnnotatedMember mutator, Object valueId, Boolean optional) + AnnotatedMember mutator, Object valueId, Boolean optional, Boolean useInput) { super(propName, type, null, mutator, PropertyMetadata.STD_OPTIONAL); _valueId = valueId; _optional = optional; + _useInput = useInput; } /** @@ -47,20 +56,27 @@ public ValueInjector(PropertyName propName, JavaType type, public ValueInjector(PropertyName propName, JavaType type, AnnotatedMember mutator, Object valueId) { - this(propName, type, mutator, valueId, null); + this(propName, type, mutator, valueId, null, null); } public Object findValue(DeserializationContext context, Object beanInstance) throws JsonMappingException { - return context.findInjectableValue(_valueId, this, beanInstance, _optional); + return context.findInjectableValue(_valueId, this, beanInstance, _optional, _useInput); } 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 'injectableValues' configured, cannot inject value with id '%s'", _valueId), + _valueId, null, beanInstance); + } + } else if (!Boolean.TRUE.equals(_useInput)) { _member.setValue(beanInstance, value); } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java index 3e76bda243..8bb0f5537d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java @@ -674,7 +674,8 @@ private Object _createUsingDelegate(AnnotatedWithParams delegateCreator, args[i] = delegate; } else { // nope, injectable: // 09-May-2025, tatu: Not sure where to get "optional" (last arg) value... - args[i] = ctxt.findInjectableValue(prop.getInjectableValueId(), prop, null, null); + args[i] = ctxt.findInjectableValue(prop.getInjectableValueId(), prop, null, null, + null); } } // and then try calling with full set of arguments 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..48ccc42a32 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381DeserializationFeatureDisabledTest.java @@ -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()); + } +} 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..0d173a8fde --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject1381WithOptionalDeserializationFeatureDisabledTest.java @@ -0,0 +1,180 @@ +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 = 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, 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/tofix/JacksonInject2678Test.java b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject2678Test.java similarity index 89% rename from src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject2678Test.java rename to src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject2678Test.java index 20f1d41abc..6de48d1ed0 100644 --- a/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject2678Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/inject/JacksonInject2678Test.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.databind.tofix; +package com.fasterxml.jackson.databind.deser.inject; import java.util.Objects; @@ -10,7 +10,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; -import com.fasterxml.jackson.databind.testutil.failure.JacksonTestFailureExpected; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -41,7 +40,6 @@ public String getField2() { } // [databind#2678] - @JacksonTestFailureExpected @Test void readValueInjectables() throws Exception { final InjectableValues injectableValues = @@ -57,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 904fd5983a..a5d399de31 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 @@ -6,7 +6,6 @@ import com.fasterxml.jackson.annotation.OptBoolean; import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import com.fasterxml.jackson.databind.exc.MissingInjectableValueExcepion; import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; @@ -23,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 { diff --git a/src/test/java/com/fasterxml/jackson/databind/introspect/IntrospectorPairTest.java b/src/test/java/com/fasterxml/jackson/databind/introspect/IntrospectorPairTest.java index 4826795da4..8648afc599 100644 --- a/src/test/java/com/fasterxml/jackson/databind/introspect/IntrospectorPairTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/introspect/IntrospectorPairTest.java @@ -699,7 +699,7 @@ static class TestInjector extends InjectableValues { @Override public Object findInjectableValue(DeserializationContext ctxt, Object valueId, - BeanProperty forProperty, Object beanInstance, Boolean optional) { + BeanProperty forProperty, Object beanInstance, Boolean optional, Boolean useInput) { if (valueId == "jjj") { UnreadableBean bean = new UnreadableBean(); bean.setValue(1); diff --git a/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject4218Test.java b/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject4218Test.java index 473f089598..cc5e889882 100644 --- a/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject4218Test.java +++ b/src/test/java/com/fasterxml/jackson/databind/tofix/JacksonInject4218Test.java @@ -40,12 +40,14 @@ public Object findInjectableValue( Object valueId, BeanProperty forProperty, Object beanInstance, - Boolean optional + Boolean optional, + Boolean useInput ) throws JsonMappingException { if (valueId.equals("id")) { return "id" + nextId++; } else { - return super.findInjectableValue(ctxt, valueId, forProperty, beanInstance, optional); + return super.findInjectableValue(ctxt, valueId, forProperty, beanInstance, + optional, useInput); } } }