Skip to content

Commit f15b71d

Browse files
authored
Part of databind#3072: Make @JacksonInject not fail when there's no corresponding value (#5131)
1 parent d899341 commit f15b71d

File tree

14 files changed

+303
-30
lines changed

14 files changed

+303
-30
lines changed

release-notes/CREDITS-2.x

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1215,10 +1215,13 @@ Oleg Chtchoukine (oshatrk@github)
12151215
to incorrect output
12161216
(2.11.1)
12171217

1218-
Joshua Shannon (retrodaredevil@github)
1218+
Lavender Shannon (retrodaredevil@github)
12191219
* Reported, contributed fix for #2785: Polymorphic subtypes not registering on copied
12201220
ObjectMapper (2.11.1)
12211221
(2.11.2)
1222+
* Requested #3072: Allow specifying `@JacksonInject` does not fail when there's no
1223+
corresponding value
1224+
(2.20.0)
12221225
12231226
Daniel Hrabovcak (TheSpiritXIII@github)
12241227
* Reported #2796: `TypeFactory.constructType()` does not take `TypeBindings` correctly
@@ -1940,3 +1943,8 @@ Will Paul (@dropofwill)
19401943
Ryan Schmitt (@rschmitt)
19411944
* Contributed #5099: Fix regression in `ObjectNode.with()`
19421945
(2.19.0)
1946+
1947+
Giulio Longfils (@giulong)
1948+
* Contributed #3072: Allow specifying `@JacksonInject` does not fail when there's no
1949+
corresponding value
1950+
(2.20.0)

release-notes/VERSION-2.x

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ Project: jackson-databind
66

77
2.20.0 (not yet released)
88

9+
#3072: Allow specifying `@JacksonInject` does not fail when there's no
10+
corresponding value
11+
(requested by Lavender S)
12+
(contributed by Giulio L)
913
#4136: Drop deprecated (in 2.12) `PropertyNamingStrategy` implementations
1014
from 2.20
1115
#5103: Use `writeStartObject(Object forValue, int size)` for `ObjectNode`

src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,14 @@
66
import java.util.*;
77
import java.util.concurrent.atomic.AtomicReference;
88

9+
import com.fasterxml.jackson.annotation.JacksonInject;
910
import com.fasterxml.jackson.annotation.JsonFormat;
1011
import com.fasterxml.jackson.annotation.ObjectIdGenerator;
1112
import com.fasterxml.jackson.annotation.ObjectIdResolver;
1213

1314
import com.fasterxml.jackson.core.*;
1415
import com.fasterxml.jackson.core.util.JacksonFeatureSet;
15-
import com.fasterxml.jackson.databind.cfg.CoercionAction;
16-
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
17-
import com.fasterxml.jackson.databind.cfg.ContextAttributes;
18-
import com.fasterxml.jackson.databind.cfg.DatatypeFeature;
19-
import com.fasterxml.jackson.databind.cfg.DatatypeFeatures;
16+
import com.fasterxml.jackson.databind.cfg.*;
2017
import com.fasterxml.jackson.databind.deser.*;
2118
import com.fasterxml.jackson.databind.deser.impl.ObjectIdReader;
2219
import com.fasterxml.jackson.databind.deser.impl.ReadableObjectId;
@@ -463,15 +460,35 @@ public final boolean hasSomeOfFeatures(int featureMask) {
463460
*/
464461
public final JsonParser getParser() { return _parser; }
465462

463+
/**
464+
* @since 2.20
465+
*/
466466
public final Object findInjectableValue(Object valueId,
467-
BeanProperty forProperty, Object beanInstance)
467+
BeanProperty forProperty, Object beanInstance, Boolean optional)
468468
throws JsonMappingException
469469
{
470470
if (_injectableValues == null) {
471+
// `optional` comes from property annotation (if any); has precedence
472+
// over global setting.
473+
if (Boolean.TRUE.equals(optional)
474+
|| (optional == null && !isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE))) {
475+
return JacksonInject.Value.empty();
476+
}
471477
return reportBadDefinition(ClassUtil.classOf(valueId), String.format(
472-
"No 'injectableValues' configured, cannot inject value with id [%s]", valueId));
478+
"No 'injectableValues' configured, cannot inject value with id '%s'", valueId));
473479
}
474-
return _injectableValues.findInjectableValue(valueId, this, forProperty, beanInstance);
480+
return _injectableValues.findInjectableValue(valueId, this, forProperty, beanInstance, optional);
481+
}
482+
483+
/**
484+
* @deprecated in 2.20
485+
*/
486+
@Deprecated // since 2.20
487+
public final Object findInjectableValue(Object valueId,
488+
BeanProperty forProperty, Object beanInstance)
489+
throws JsonMappingException
490+
{
491+
return findInjectableValue(valueId, forProperty, beanInstance, null);
475492
}
476493

477494
/**

src/main/java/com/fasterxml/jackson/databind/DeserializationFeature.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.fasterxml.jackson.databind;
22

3+
import com.fasterxml.jackson.annotation.JacksonInject;
34
import com.fasterxml.jackson.databind.cfg.ConfigFeature;
45
import com.fasterxml.jackson.databind.exc.InvalidNullException;
56

@@ -266,7 +267,7 @@ public enum DeserializationFeature implements ConfigFeature
266267
* white space or comments, if supported by data format).
267268
*<p>
268269
* Feature is disabled by default (so that no check is made for possible trailing
269-
* token(s)) for backwards compatibility reasons.
270+
* token(s)) for backwards-compatibility reasons.
270271
*
271272
* @since 2.9
272273
*/
@@ -336,6 +337,22 @@ public enum DeserializationFeature implements ConfigFeature
336337
*/
337338
FAIL_ON_UNEXPECTED_VIEW_PROPERTIES(false),
338339

340+
/**
341+
* Feature that determines the handling of injected properties during deserialization.
342+
*<p>
343+
* When enabled, if an injected property without matching value is encountered
344+
* during deserialization, an exception is thrown.
345+
* When disabled, no exception is thrown.
346+
* See {@link JacksonInject#optional()} for per-property override
347+
* of this setting.
348+
*<p>
349+
* This feature is enabled by default to maintain backwards-compatibility.
350+
*
351+
* @see JacksonInject#optional()
352+
* @since 2.20
353+
*/
354+
FAIL_ON_UNKNOWN_INJECT_VALUE(true),
355+
339356
/*
340357
/******************************************************
341358
/* Structural conversion features

src/main/java/com/fasterxml/jackson/databind/InjectableValues.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,16 @@ public abstract class InjectableValues
2323
* @param forProperty Bean property in which value is to be injected
2424
* @param beanInstance Bean instance that contains property to inject,
2525
* if available; null if bean has not yet been constructed.
26+
* @param optional Flag used for configuring the behavior when the value
27+
* to inject is not found
2628
*/
29+
public abstract Object findInjectableValue(Object valueId, DeserializationContext ctxt,
30+
BeanProperty forProperty, Object beanInstance, Boolean optional) throws JsonMappingException;
31+
32+
/**
33+
* @deprecated in 2.20
34+
*/
35+
@Deprecated // since 2.20
2736
public abstract Object findInjectableValue(Object valueId, DeserializationContext ctxt,
2837
BeanProperty forProperty, Object beanInstance) throws JsonMappingException;
2938

@@ -63,9 +72,13 @@ public Std addValue(Class<?> classKey, Object value) {
6372
return this;
6473
}
6574

75+
/**
76+
* @since 2.20
77+
*/
6678
@Override
6779
public Object findInjectableValue(Object valueId, DeserializationContext ctxt,
68-
BeanProperty forProperty, Object beanInstance) throws JsonMappingException
80+
BeanProperty forProperty, Object beanInstance, Boolean optional)
81+
throws JsonMappingException
6982
{
7083
if (!(valueId instanceof String)) {
7184
ctxt.reportBadDefinition(ClassUtil.classOf(valueId),
@@ -76,9 +89,26 @@ public Object findInjectableValue(Object valueId, DeserializationContext ctxt,
7689
String key = (String) valueId;
7790
Object ob = _values.get(key);
7891
if (ob == null && !_values.containsKey(key)) {
79-
throw new IllegalArgumentException("No injectable value with id '"+key+"' found (for property '"+forProperty.getName()+"')");
92+
if (Boolean.FALSE.equals(optional)
93+
|| ((optional == null)
94+
&& ctxt.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE))) {
95+
return ctxt.reportBadDefinition(ClassUtil.classOf(valueId), String.format(
96+
"No injectable value with id '" + key + "' " +
97+
"found (for property '" + forProperty.getName() + "')"));
98+
}
8099
}
81100
return ob;
82101
}
102+
103+
/**
104+
* @deprecated in 2.20
105+
*/
106+
@Override
107+
@Deprecated // since 2.20
108+
public Object findInjectableValue(Object valueId, DeserializationContext ctxt,
109+
BeanProperty forProperty, Object beanInstance) throws JsonMappingException
110+
{
111+
return this.findInjectableValue(valueId, ctxt, forProperty, beanInstance, null);
112+
}
83113
}
84114
}

src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBuilder.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,13 +239,16 @@ public void addBackReferenceProperty(String referenceName, SettableBeanProperty
239239
*/
240240
}
241241

242+
/**
243+
* @since 2.20
244+
*/
242245
public void addInjectable(PropertyName propName, JavaType propType,
243246
Annotations contextAnnotations, AnnotatedMember member,
244-
Object valueId)
247+
Object valueId, Boolean optional)
245248
throws JsonMappingException
246249
{
247250
if (_injectables == null) {
248-
_injectables = new ArrayList<ValueInjector>();
251+
_injectables = new ArrayList<>();
249252
}
250253
if ( _config.canOverrideAccessModifiers()) {
251254
try {
@@ -254,7 +257,19 @@ public void addInjectable(PropertyName propName, JavaType propType,
254257
_handleBadAccess(e);
255258
}
256259
}
257-
_injectables.add(new ValueInjector(propName, propType, member, valueId));
260+
_injectables.add(new ValueInjector(propName, propType, member, valueId, optional));
261+
}
262+
263+
/**
264+
* @deprecated in 2.20
265+
*/
266+
@Deprecated // since 2.20
267+
public void addInjectable(PropertyName propName, JavaType propType,
268+
Annotations contextAnnotations, AnnotatedMember member,
269+
Object valueId)
270+
throws JsonMappingException
271+
{
272+
this.addInjectable(propName, propType, contextAnnotations, member, valueId, null);
258273
}
259274

260275
/**

src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -812,11 +812,16 @@ protected void addInjectables(DeserializationContext ctxt,
812812
{
813813
Map<Object, AnnotatedMember> raw = beanDesc.findInjectables();
814814
if (raw != null) {
815+
final AnnotationIntrospector introspector = ctxt.getAnnotationIntrospector();
816+
815817
for (Map.Entry<Object, AnnotatedMember> entry : raw.entrySet()) {
816818
AnnotatedMember m = entry.getValue();
819+
final JacksonInject.Value injectableValue = introspector.findInjectableValue(m);
820+
final Boolean optional = injectableValue == null ? null : injectableValue.getOptional();
821+
817822
builder.addInjectable(PropertyName.construct(m.getName()),
818823
m.getType(),
819-
beanDesc.getClassAnnotations(), m, entry.getKey());
824+
beanDesc.getClassAnnotations(), m, entry.getKey(), optional);
820825
}
821826
}
822827
}

src/main/java/com/fasterxml/jackson/databind/deser/CreatorProperty.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public CreatorProperty(PropertyName name, JavaType type, PropertyName wrapperNam
103103
{
104104
this(name, type, wrapperName, typeDeser, contextAnnotations, param, index,
105105
(injectableValueId == null) ? null
106-
: JacksonInject.Value.construct(injectableValueId, null),
106+
: JacksonInject.Value.construct(injectableValueId, null, null),
107107
metadata);
108108
}
109109

src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingExcep
269269
Object injectableValueId = prop.getInjectableValueId();
270270
if (injectableValueId != null) {
271271
return _context.findInjectableValue(prop.getInjectableValueId(),
272-
prop, null);
272+
prop, null, null);
273273
}
274274
// Second: required?
275275
if (prop.isRequired()) {

src/main/java/com/fasterxml/jackson/databind/deser/impl/ValueInjector.java

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.io.IOException;
44

5+
import com.fasterxml.jackson.annotation.JacksonInject;
56
import com.fasterxml.jackson.databind.*;
67
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
78

@@ -21,33 +22,46 @@ public class ValueInjector
2122
*/
2223
protected final Object _valueId;
2324

25+
/**
26+
* Flag used for configuring the behavior when the value to inject is not found.
27+
*
28+
* @since 2.20
29+
*/
30+
protected final Boolean _optional;
31+
32+
/**
33+
* @since 2.20
34+
*/
2435
public ValueInjector(PropertyName propName, JavaType type,
25-
AnnotatedMember mutator, Object valueId)
36+
AnnotatedMember mutator, Object valueId, Boolean optional)
2637
{
2738
super(propName, type, null, mutator, PropertyMetadata.STD_OPTIONAL);
2839
_valueId = valueId;
40+
_optional = optional;
2941
}
3042

3143
/**
32-
* @deprecated in 2.9 (remove from 3.0)
44+
* @deprecated in 2.20 (remove from 3.0)
3345
*/
34-
@Deprecated // see [databind#1835]
46+
@Deprecated // since 2.20
3547
public ValueInjector(PropertyName propName, JavaType type,
36-
com.fasterxml.jackson.databind.util.Annotations contextAnnotations, // removed from later versions
3748
AnnotatedMember mutator, Object valueId)
3849
{
39-
this(propName, type, mutator, valueId);
50+
this(propName, type, mutator, valueId, null);
4051
}
4152

4253
public Object findValue(DeserializationContext context, Object beanInstance)
4354
throws JsonMappingException
4455
{
45-
return context.findInjectableValue(_valueId, this, beanInstance);
56+
return context.findInjectableValue(_valueId, this, beanInstance, _optional);
4657
}
4758

4859
public void inject(DeserializationContext context, Object beanInstance)
4960
throws IOException
5061
{
51-
_member.setValue(beanInstance, findValue(context, beanInstance));
62+
final Object value = findValue(context, beanInstance);
63+
if (!JacksonInject.Value.empty().equals(value)) {
64+
_member.setValue(beanInstance, value);
65+
}
5266
}
53-
}
67+
}

src/main/java/com/fasterxml/jackson/databind/deser/std/StdValueInstantiator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,8 @@ private Object _createUsingDelegate(AnnotatedWithParams delegateCreator,
673673
if (prop == null) { // delegate
674674
args[i] = delegate;
675675
} else { // nope, injectable:
676-
args[i] = ctxt.findInjectableValue(prop.getInjectableValueId(), prop, null);
676+
// 09-May-2025, tatu: Not sure where to get "optional" (last arg) value...
677+
args[i] = ctxt.findInjectableValue(prop.getInjectableValueId(), prop, null, null);
677678
}
678679
}
679680
// and then try calling with full set of arguments

0 commit comments

Comments
 (0)