Skip to content

feat(#1381): Add a way to specify "inject-only" with @JacksonInject #5175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: 2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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)

6 changes: 6 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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));
}

/**
Expand All @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -822,11 +822,11 @@ protected void addInjectables(DeserializationContext ctxt,
for (Map.Entry<Object, AnnotatedMember> entry : raw.entrySet()) {
AnnotatedMember m = entry.getValue();
final JacksonInject.Value injectableValue = introspector.findInjectableValue(m);
final Boolean optional = injectableValue == null ? null : injectableValue.getOptional();

builder.addInjectable(PropertyName.construct(m.getName()),
m.getType(),
beanDesc.getClassAnnotations(), m, entry.getKey(), optional);
beanDesc.getClassAnnotations(), m, entry.getKey(),
injectableValue.getOptional(), injectableValue.getUseInput());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -218,6 +219,25 @@ public Object[] getParameters(SettableBeanProperty[] props)
if (_anyParamSetter != null) {
_creatorParameters[_anyParamSetter.getParameterIndex()] = _createAndSetAnySetterValue();
}

for (int ix = 0; ix < props.length; ++ix) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the moment when creator properties are resolved, so I think it's ok to have the logic here, as per the method's javadoc as well:

/**
* Method called to do necessary post-processing such as injection of values
* and verification of values for required properties,
* after either {@link #assignParameter(SettableBeanProperty, Object)}
* returns true (to indicate all creator properties are found), or when
* then whole JSON Object has been processed,
*/
public Object[] getParameters(SettableBeanProperty[] props)

I placed the new part right before the code that checks for null creator properties in case we have an injected value to use, so to not fail. Should I move it after?

final SettableBeanProperty prop = props[ix];
final 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) {
Expand Down Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading