Skip to content

Commit c05c071

Browse files
authored
feat(#1381): Add a way to specify "inject-only" with @JacksonInject (#5175)
1 parent 555ce3f commit c05c071

File tree

12 files changed

+795
-25
lines changed

12 files changed

+795
-25
lines changed

release-notes/CREDITS-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1966,6 +1966,8 @@ Giulio Longfils (@giulong)
19661966
* Contributed #4218: If `@JacksonInject` is specified for field and deserialized by
19671967
the Creator, the inject process will be executed twice
19681968
(2.20.0)
1969+
* Contributed #1381: Add a way to specify "inject-only" with `@JacksonInject`
1970+
(2.21.0)
19691971

19701972
Plamen Tanov (@ptanov)
19711973
* Reported #2678: `@JacksonInject` added to property overrides value from the JSON

release-notes/VERSION-2.x

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

77
2.21.0 (not yet released)
88

9+
#1381: Add a way to specify "inject-only" with `@JacksonInject`
10+
(fix by Giulio L)
911
#1547: Un-deprecate `SerializationFeature.WRITE_EMPTY_JSON_ARRAYS`
1012
#5045: If there is a no-parameter constructor marked as `JsonCreator` and
1113
a constructor reported as `DefaultCreator`, latter is incorrectly used

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,10 +469,12 @@ public final Object findInjectableValue(Object valueId,
469469
throws JsonMappingException
470470
{
471471
if (_injectableValues == null) {
472-
// `optional` comes from property annotation (if any); has precedence
473-
// over global setting.
474-
if (Boolean.TRUE.equals(optional)
475-
|| (optional == null && !isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE))) {
472+
// `useInput` and `optional` come from property annotation (if any);
473+
// they have precedence over global setting.
474+
if (Boolean.TRUE.equals(useInput)
475+
|| Boolean.TRUE.equals(optional)
476+
|| ((useInput == null || optional == null)
477+
&& !isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE))) {
476478
return JacksonInject.Value.empty();
477479
}
478480
throw missingInjectableValueException(String.format(

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

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.io.IOException;
44
import java.util.BitSet;
55

6+
import com.fasterxml.jackson.annotation.JacksonInject;
67
import com.fasterxml.jackson.core.JsonParser;
78
import com.fasterxml.jackson.databind.*;
89
import com.fasterxml.jackson.databind.deser.*;
@@ -90,7 +91,8 @@ public class PropertyValueBuffer
9091
protected PropertyValue _anyParamBuffered;
9192

9293
/**
93-
* Indexes properties that are injectable, if any; {@code null} if none.
94+
* Indexes properties that are injectable, if any; {@code null} if none,
95+
* cleared as they are injected.
9496
*
9597
* @since 2.21
9698
*/
@@ -229,8 +231,19 @@ public Object[] getParameters(SettableBeanProperty[] props)
229231
if (_anyParamSetter != null) {
230232
_creatorParameters[_anyParamSetter.getParameterIndex()] = _createAndSetAnySetterValue();
231233
}
234+
235+
// [databind#1381] handle inject-only (useInput = false) properties
236+
if (_injectablePropIndexes != null) {
237+
int ix = _injectablePropIndexes.nextSetBit(0);
238+
while (ix >= 0) {
239+
_inject(props[ix]);
240+
ix = _injectablePropIndexes.nextSetBit(ix + 1);
241+
}
242+
}
243+
232244
if (_context.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) {
233-
for (int ix = 0; ix < props.length; ++ix) {
245+
final int len = _creatorParameters.length;
246+
for (int ix = 0; ix < len; ++ix) {
234247
if (_creatorParameters[ix] == null) {
235248
SettableBeanProperty prop = props[ix];
236249
_context.reportInputMismatch(prop,
@@ -239,6 +252,7 @@ public Object[] getParameters(SettableBeanProperty[] props)
239252
}
240253
}
241254
}
255+
242256
return _creatorParameters;
243257
}
244258

@@ -279,6 +293,8 @@ protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingExcep
279293
// First: do we have injectable value?
280294
Object injectableValueId = prop.getInjectableValueId();
281295
if (injectableValueId != null) {
296+
// 10-Nov-2025: [databind#1381] Is this needed?
297+
_injectablePropIndexes.clear(prop.getCreatorIndex());
282298
return _context.findInjectableValue(prop.getInjectableValueId(),
283299
prop, null, null, null);
284300
}
@@ -313,6 +329,30 @@ protected Object _findMissing(SettableBeanProperty prop) throws JsonMappingExcep
313329
}
314330
}
315331

332+
/**
333+
* Method called to inject value for given property, possibly overriding
334+
* assigned (from input) value.
335+
*
336+
* @since 2.21
337+
*/
338+
private void _inject(final SettableBeanProperty prop) throws JsonMappingException {
339+
final JacksonInject.Value injection = prop.getInjectionDefinition();
340+
341+
if (injection != null) {
342+
final Boolean useInput = injection.getUseInput();
343+
344+
if (!Boolean.TRUE.equals(useInput)) {
345+
final Object value = _context.findInjectableValue(injection.getId(),
346+
prop, prop.getMember(), injection.getOptional(), useInput);
347+
348+
if (value != JacksonInject.Value.empty()) {
349+
int ix = prop.getCreatorIndex();
350+
_creatorParameters[ix] = value;
351+
}
352+
}
353+
}
354+
}
355+
316356
/*
317357
/**********************************************************
318358
/* Other methods
@@ -392,6 +432,7 @@ public boolean assignParameter(SettableBeanProperty prop, Object value)
392432
_paramsSeenBig.set(ix);
393433
if (--_paramsNeeded <= 0) {
394434
// 29-Nov-2016, tatu: But! May still require Object Id value
435+
return (_objectIdReader == null) || (_idValue != null);
395436
}
396437
}
397438
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,15 @@ public void inject(DeserializationContext context, Object beanInstance)
6969
throws IOException
7070
{
7171
final Object value = findValue(context, beanInstance);
72-
if (!JacksonInject.Value.empty().equals(value)) {
72+
73+
if (value == JacksonInject.Value.empty()) {
74+
if (Boolean.FALSE.equals(_optional)) {
75+
throw context.missingInjectableValueException(
76+
String.format("No injectable value with id '%s' found (for property '%s')",
77+
_valueId, getName()),
78+
_valueId, null, beanInstance);
79+
}
80+
} else if (!Boolean.TRUE.equals(_useInput)) {
7381
_member.setValue(beanInstance, value);
7482
}
7583
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package com.fasterxml.jackson.databind.deser.inject;
2+
3+
import com.fasterxml.jackson.annotation.JacksonInject;
4+
import com.fasterxml.jackson.annotation.JsonCreator;
5+
import com.fasterxml.jackson.annotation.JsonProperty;
6+
import com.fasterxml.jackson.annotation.OptBoolean;
7+
import com.fasterxml.jackson.databind.DeserializationFeature;
8+
import com.fasterxml.jackson.databind.InjectableValues;
9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import com.fasterxml.jackson.databind.exc.ValueInstantiationException;
11+
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
12+
import org.junit.jupiter.api.DisplayName;
13+
import org.junit.jupiter.api.Test;
14+
15+
import static org.junit.jupiter.api.Assertions.assertEquals;
16+
import static org.junit.jupiter.api.Assertions.assertThrows;
17+
18+
class JacksonInject1381DeserializationFeatureDisabledTest extends DatabindTestUtil {
19+
static class InputDefault {
20+
@JacksonInject(value = "key")
21+
@JsonProperty("field")
22+
private final String _field;
23+
24+
@JsonCreator
25+
public InputDefault(@JsonProperty("field") final String field) {
26+
_field = field;
27+
}
28+
29+
public String getField() {
30+
return _field;
31+
}
32+
}
33+
34+
static class InputDefaultConstructor {
35+
private final String _field;
36+
37+
@JsonCreator
38+
public InputDefaultConstructor(@JacksonInject(value = "key")
39+
@JsonProperty("field") final String field) {
40+
_field = field;
41+
}
42+
43+
public String getField() {
44+
return _field;
45+
}
46+
}
47+
48+
static class InputTrue {
49+
@JacksonInject(value = "key", useInput = OptBoolean.TRUE)
50+
@JsonProperty("field")
51+
private final String _field;
52+
53+
@JsonCreator
54+
public InputTrue(@JsonProperty("field") final String field) {
55+
_field = field;
56+
}
57+
58+
public String getField() {
59+
return _field;
60+
}
61+
}
62+
63+
static class InputTrueConstructor {
64+
private final String _field;
65+
66+
@JsonCreator
67+
public InputTrueConstructor(@JacksonInject(value = "key", useInput = OptBoolean.TRUE)
68+
@JsonProperty("field") final String field) {
69+
_field = field;
70+
}
71+
72+
public String getField() {
73+
return _field;
74+
}
75+
76+
}
77+
78+
static class InputFalse {
79+
@JacksonInject(value = "key", useInput = OptBoolean.FALSE)
80+
@JsonProperty("field")
81+
private final String _field;
82+
83+
@JsonCreator
84+
public InputFalse(@JsonProperty("field") final String field) {
85+
_field = field;
86+
}
87+
88+
public String getField() {
89+
return _field;
90+
}
91+
}
92+
93+
static class InputFalseConstructor {
94+
private final String _field;
95+
96+
@JsonCreator
97+
public InputFalseConstructor(@JacksonInject(value = "key", useInput = OptBoolean.FALSE)
98+
@JsonProperty("field") final String field) {
99+
_field = field;
100+
}
101+
102+
public String getField() {
103+
return _field;
104+
}
105+
}
106+
107+
private final String empty = "{}";
108+
private final String input = "{\"field\": \"input\"}";
109+
110+
private final ObjectMapper plainMapper = jsonMapperBuilder()
111+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE)
112+
.build();
113+
private final ObjectMapper injectedMapper = jsonMapperBuilder()
114+
.injectableValues(new InjectableValues.Std().addValue("key", "injected"))
115+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_INJECT_VALUE)
116+
.build();
117+
118+
@Test
119+
@DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input NO, injectable NO, useInput DEFAULT|TRUE|FALSE => exception")
120+
void test1() {
121+
assertThrows(ValueInstantiationException.class,
122+
() -> plainMapper.readValue(empty, InputDefault.class));
123+
assertThrows(ValueInstantiationException.class,
124+
() -> plainMapper.readValue(empty, InputDefaultConstructor.class));
125+
126+
assertThrows(ValueInstantiationException.class,
127+
() -> plainMapper.readValue(empty, InputTrue.class));
128+
assertThrows(ValueInstantiationException.class,
129+
() -> plainMapper.readValue(empty, InputTrueConstructor.class));
130+
131+
assertThrows(ValueInstantiationException.class,
132+
() -> plainMapper.readValue(empty, InputFalse.class));
133+
assertThrows(ValueInstantiationException.class,
134+
() -> plainMapper.readValue(empty, InputFalseConstructor.class));
135+
}
136+
137+
@Test
138+
@DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input NO, injectable YES, useInput DEFAULT|TRUE|FALSE => injected")
139+
void test2() throws Exception {
140+
assertEquals("injected", injectedMapper.readValue(empty, InputDefault.class).getField());
141+
assertEquals("injected", injectedMapper.readValue(empty, InputDefaultConstructor.class).getField());
142+
assertEquals("injected", injectedMapper.readValue(empty, InputTrue.class).getField());
143+
assertEquals("injected", injectedMapper.readValue(empty, InputTrueConstructor.class).getField());
144+
assertEquals("injected", injectedMapper.readValue(empty, InputFalse.class).getField());
145+
assertEquals("injected", injectedMapper.readValue(empty, InputFalseConstructor.class).getField());
146+
}
147+
148+
@Test
149+
@DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input YES, injectable NO, useInput DEFAULT|FALSE => exception")
150+
void test3() throws Exception {
151+
assertEquals("input", plainMapper.readValue(input, InputDefault.class).getField());
152+
assertEquals("input", plainMapper.readValue(input, InputDefaultConstructor.class).getField());
153+
assertEquals("input", plainMapper.readValue(input, InputFalse.class).getField());
154+
assertEquals("input", plainMapper.readValue(input, InputFalseConstructor.class).getField());
155+
}
156+
157+
@Test
158+
@DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input YES, injectable NO, useInput TRUE => input")
159+
void test4() throws Exception {
160+
assertEquals("input", plainMapper.readValue(input, InputTrue.class).getField());
161+
assertEquals("input", plainMapper.readValue(input, InputTrueConstructor.class).getField());
162+
}
163+
164+
@Test
165+
@DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input YES, injectable YES, useInput DEFAULT|FALSE => injected")
166+
void test5() throws Exception {
167+
assertEquals("injected", injectedMapper.readValue(input, InputDefault.class).getField());
168+
assertEquals("injected", injectedMapper.readValue(input, InputDefaultConstructor.class).getField());
169+
assertEquals("injected", injectedMapper.readValue(input, InputFalse.class).getField());
170+
assertEquals("injected", injectedMapper.readValue(input, InputFalseConstructor.class).getField());
171+
}
172+
173+
@Test
174+
@DisplayName("FAIL_ON_UNKNOWN_INJECT_VALUE NO, input YES, injectable YES, useInput TRUE => input")
175+
void test6() throws Exception {
176+
assertEquals("input", injectedMapper.readValue(input, InputTrue.class).getField());
177+
assertEquals("input", injectedMapper.readValue(input, InputTrueConstructor.class).getField());
178+
}
179+
}

0 commit comments

Comments
 (0)