Skip to content

Commit 052ef9b

Browse files
authored
Add optional property for @JacksonInject to allow optionally injected values (#291)
1 parent 258a52e commit 052ef9b

File tree

3 files changed

+106
-24
lines changed

3 files changed

+106
-24
lines changed

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ NOTE: Annotations module will never contain changes in patch versions,
1313

1414
2.20.0 (not yet released)
1515

16+
#291: Add `optional` property for `@JacksonInject` to allow optionally injected values
17+
(contributed by @giulong)
1618
- Generate SBOMs [JSTEP-14]
1719

1820
2.19.0 (24-Apr-2025)

src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,22 @@
4242
*/
4343
public OptBoolean useInput() default OptBoolean.DEFAULT;
4444

45+
/**
46+
* Whether to throw an exception when the {@code ObjectMapper} does not find
47+
* the value to inject.
48+
*<p>
49+
* Default is {@code OptBoolean.DEFAULT} for backwards-compatibility: in this
50+
* case {@code ObjectMapper} defaults are used (which in turn are same
51+
* as {code OptBoolean.FALSE}).
52+
*
53+
* @return {@link OptBoolean#FALSE} to throw an exception; {@link OptBoolean#TRUE}
54+
* to avoid throwing it; or {@link OptBoolean#DEFAULT} to use configure defaults
55+
* (which are same as {@link OptBoolean#FALSE} for Jackson 2.x)
56+
*
57+
* @since 2.20
58+
*/
59+
public OptBoolean optional() default OptBoolean.DEFAULT;
60+
4561
/*
4662
/**********************************************************
4763
/* Value class used to enclose information, allow for
@@ -63,7 +79,7 @@ public static class Value
6379
{
6480
private static final long serialVersionUID = 1L;
6581

66-
protected final static Value EMPTY = new Value(null, null);
82+
protected final static Value EMPTY = new Value(null, null, null);
6783

6884
/**
6985
* Id to use to access injected value; if `null`, "default" name, derived
@@ -73,9 +89,12 @@ public static class Value
7389

7490
protected final Boolean _useInput;
7591

76-
protected Value(Object id, Boolean useInput) {
92+
protected final Boolean _optional;
93+
94+
protected Value(Object id, Boolean useInput, Boolean optional) {
7795
_id = id;
7896
_useInput = useInput;
97+
_optional = optional;
7998
}
8099

81100
@Override
@@ -93,25 +112,33 @@ public static Value empty() {
93112
return EMPTY;
94113
}
95114

115+
@Deprecated //since 2.20
96116
public static Value construct(Object id, Boolean useInput) {
117+
return construct(id, useInput, null);
118+
}
119+
120+
/**
121+
* @since 2.20
122+
*/
123+
public static Value construct(Object id, Boolean useInput, Boolean optional) {
97124
if ("".equals(id)) {
98125
id = null;
99126
}
100-
if (_empty(id, useInput)) {
127+
if (_empty(id, useInput, optional)) {
101128
return EMPTY;
102129
}
103-
return new Value(id, useInput);
130+
return new Value(id, useInput, optional);
104131
}
105132

106133
public static Value from(JacksonInject src) {
107134
if (src == null) {
108135
return EMPTY;
109136
}
110-
return construct(src.value(), src.useInput().asBoolean());
137+
return construct(src.value(), src.useInput().asBoolean(), src.optional().asBoolean());
111138
}
112139

113140
public static Value forId(Object id) {
114-
return construct(id, null);
141+
return construct(id, null, null);
115142
}
116143

117144
/*
@@ -128,7 +155,7 @@ public Value withId(Object id) {
128155
} else if (id.equals(_id)) {
129156
return this;
130157
}
131-
return new Value(id, _useInput);
158+
return new Value(id, _useInput, _optional);
132159
}
133160

134161
public Value withUseInput(Boolean useInput) {
@@ -139,7 +166,18 @@ public Value withUseInput(Boolean useInput) {
139166
} else if (useInput.equals(_useInput)) {
140167
return this;
141168
}
142-
return new Value(_id, useInput);
169+
return new Value(_id, useInput, _optional);
170+
}
171+
172+
public Value withOptional(Boolean optional) {
173+
if (optional == null) {
174+
if (_optional == null) {
175+
return this;
176+
}
177+
} else if (optional.equals(_optional)) {
178+
return this;
179+
}
180+
return new Value(_id, _useInput, optional);
143181
}
144182

145183
/*
@@ -150,6 +188,7 @@ public Value withUseInput(Boolean useInput) {
150188

151189
public Object getId() { return _id; }
152190
public Boolean getUseInput() { return _useInput; }
191+
public Boolean getOptional() { return _optional; }
153192

154193
public boolean hasId() {
155194
return _id != null;
@@ -167,8 +206,8 @@ public boolean willUseInput(boolean defaultSetting) {
167206

168207
@Override
169208
public String toString() {
170-
return String.format("JacksonInject.Value(id=%s,useInput=%s)",
171-
_id, _useInput);
209+
return String.format("JacksonInject.Value(id=%s,useInput=%s,optional=%s)",
210+
_id, _useInput, _optional);
172211
}
173212

174213
@Override
@@ -180,6 +219,9 @@ public int hashCode() {
180219
if (_useInput != null) {
181220
h += _useInput.hashCode();
182221
}
222+
if (_optional != null) {
223+
h += _optional.hashCode();
224+
}
183225
return h;
184226
}
185227

@@ -189,12 +231,13 @@ public boolean equals(Object o) {
189231
if (o == null) return false;
190232
if (o.getClass() == getClass()) {
191233
Value other = (Value) o;
192-
if (OptBoolean.equals(_useInput, other._useInput)) {
193-
if (_id == null) {
194-
return other._id == null;
195-
}
196-
return _id.equals(other._id);
197-
}
234+
235+
return (_id == null && other._id == null
236+
|| _id != null && _id.equals(other._id))
237+
&& (_useInput == null && other._useInput == null
238+
|| _useInput != null && _useInput.equals(other._useInput))
239+
&& (_optional == null && other._optional == null
240+
|| _optional != null && _optional.equals(other._optional));
198241
}
199242
return false;
200243
}
@@ -205,8 +248,8 @@ public boolean equals(Object o) {
205248
/**********************************************************
206249
*/
207250

208-
private static boolean _empty(Object id, Boolean useInput) {
209-
return (id == null) && (useInput == null);
251+
private static boolean _empty(Object id, Boolean useInput, Boolean optional) {
252+
return (id == null) && (useInput == null) && optional == null;
210253
}
211254
}
212255
}

src/test/java/com/fasterxml/jackson/annotation/JacksonInjectTest.java

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77
public class JacksonInjectTest
88
{
99
private final static class Bogus {
10-
@JacksonInject(value="inject", useInput=OptBoolean.FALSE)
10+
@JacksonInject(value="inject", useInput=OptBoolean.FALSE,
11+
optional=OptBoolean.FALSE)
1112
public int field;
1213

1314
@JacksonInject
1415
public int vanilla;
16+
17+
@JacksonInject(optional = OptBoolean.TRUE)
18+
public int optionalField;
1519
}
1620

1721
private final JacksonInject.Value EMPTY = JacksonInject.Value.empty();
@@ -24,9 +28,9 @@ public void testEmpty()
2428
assertTrue(EMPTY.willUseInput(true));
2529
assertFalse(EMPTY.willUseInput(false));
2630

27-
assertSame(EMPTY, JacksonInject.Value.construct(null, null));
31+
assertSame(EMPTY, JacksonInject.Value.construct(null, null, null));
2832
// also, "" gets coerced to null so
29-
assertSame(EMPTY, JacksonInject.Value.construct("", null));
33+
assertSame(EMPTY, JacksonInject.Value.construct("", null, null));
3034
}
3135

3236
@Test
@@ -39,18 +43,25 @@ public void testFromAnnotation() throws Exception
3943
assertEquals("inject", v.getId());
4044
assertEquals(Boolean.FALSE, v.getUseInput());
4145

42-
assertEquals("JacksonInject.Value(id=inject,useInput=false)", v.toString());
46+
assertEquals("JacksonInject.Value(id=inject,useInput=false,optional=false)", v.toString());
4347
assertFalse(v.equals(EMPTY));
4448
assertFalse(EMPTY.equals(v));
4549

4650
JacksonInject ann2 = Bogus.class.getField("vanilla").getAnnotation(JacksonInject.class);
4751
v = JacksonInject.Value.from(ann2);
48-
assertSame(EMPTY, v);
52+
assertEquals(JacksonInject.Value.construct(null, null, null), v,
53+
"optional should be `null` by default");
54+
55+
JacksonInject optionalField = Bogus.class.getField("optionalField")
56+
.getAnnotation(JacksonInject.class);
57+
v = JacksonInject.Value.from(optionalField);
58+
assertEquals(JacksonInject.Value.construct(null, null, true), v);
4959
}
5060

61+
@SuppressWarnings("unlikely-arg-type")
5162
@Test
5263
public void testStdMethods() {
53-
assertEquals("JacksonInject.Value(id=null,useInput=null)",
64+
assertEquals("JacksonInject.Value(id=null,useInput=null,optional=null)",
5465
EMPTY.toString());
5566
int x = EMPTY.hashCode();
5667
if (x == 0) { // no fixed value, but should not evaluate to 0
@@ -59,6 +70,25 @@ public void testStdMethods() {
5970
assertEquals(EMPTY, EMPTY);
6071
assertFalse(EMPTY.equals(null));
6172
assertFalse(EMPTY.equals("xyz"));
73+
74+
JacksonInject.Value equals1 = JacksonInject.Value.construct("value", true, true);
75+
JacksonInject.Value equals2 = JacksonInject.Value.construct("value", true, true);
76+
JacksonInject.Value valueNull = JacksonInject.Value.construct(null, true, true);
77+
JacksonInject.Value useInputNull = JacksonInject.Value.construct("value", null, true);
78+
JacksonInject.Value optionalNull = JacksonInject.Value.construct("value", true, null);
79+
JacksonInject.Value valueNotEqual = JacksonInject.Value.construct("not equal", true, true);
80+
JacksonInject.Value useInputNotEqual = JacksonInject.Value.construct("value", false, true);
81+
JacksonInject.Value optionalNotEqual = JacksonInject.Value.construct("value", true, false);
82+
String string = "string";
83+
84+
assertEquals(equals1, equals2);
85+
assertNotEquals(equals1, valueNull);
86+
assertNotEquals(equals1, useInputNull);
87+
assertNotEquals(equals1, optionalNull);
88+
assertNotEquals(equals1, valueNotEqual);
89+
assertNotEquals(equals1, useInputNotEqual);
90+
assertNotEquals(equals1, optionalNotEqual);
91+
assertNotEquals(equals1, string);
6292
}
6393

6494
@Test
@@ -75,6 +105,13 @@ public void testFactories() throws Exception
75105
assertFalse(v2.equals(v));
76106
assertSame(v2, v2.withUseInput(Boolean.TRUE));
77107

108+
JacksonInject.Value v3 = v.withOptional(Boolean.TRUE);
109+
assertNotSame(v, v3);
110+
assertFalse(v.equals(v3));
111+
assertFalse(v3.equals(v));
112+
assertSame(v3, v3.withOptional(Boolean.TRUE));
113+
assertTrue(v3.getOptional());
114+
78115
int x = v2.hashCode();
79116
if (x == 0) { // no fixed value, but should not evaluate to 0
80117
fail();

0 commit comments

Comments
 (0)