Skip to content

Commit 66ac813

Browse files
committed
Merge branch '2.x' into 3.x
2 parents 7b2c29e + 052ef9b commit 66ac813

3 files changed

Lines changed: 106 additions & 24 deletions

File tree

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
@@ -39,6 +39,22 @@
3939
*/
4040
public OptBoolean useInput() default OptBoolean.DEFAULT;
4141

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

61-
protected final static Value EMPTY = new Value(null, null);
77+
protected final static Value EMPTY = new Value(null, null, null);
6278

6379
/**
6480
* Id to use to access injected value; if `null`, "default" name, derived
@@ -68,9 +84,12 @@ public static class Value
6884

6985
protected final Boolean _useInput;
7086

71-
protected Value(Object id, Boolean useInput) {
87+
protected final Boolean _optional;
88+
89+
protected Value(Object id, Boolean useInput, Boolean optional) {
7290
_id = id;
7391
_useInput = useInput;
92+
_optional = optional;
7493
}
7594

7695
@Override
@@ -88,25 +107,33 @@ public static Value empty() {
88107
return EMPTY;
89108
}
90109

110+
@Deprecated //since 2.20
91111
public static Value construct(Object id, Boolean useInput) {
112+
return construct(id, useInput, null);
113+
}
114+
115+
/**
116+
* @since 2.20
117+
*/
118+
public static Value construct(Object id, Boolean useInput, Boolean optional) {
92119
if ("".equals(id)) {
93120
id = null;
94121
}
95-
if (_empty(id, useInput)) {
122+
if (_empty(id, useInput, optional)) {
96123
return EMPTY;
97124
}
98-
return new Value(id, useInput);
125+
return new Value(id, useInput, optional);
99126
}
100127

101128
public static Value from(JacksonInject src) {
102129
if (src == null) {
103130
return EMPTY;
104131
}
105-
return construct(src.value(), src.useInput().asBoolean());
132+
return construct(src.value(), src.useInput().asBoolean(), src.optional().asBoolean());
106133
}
107134

108135
public static Value forId(Object id) {
109-
return construct(id, null);
136+
return construct(id, null, null);
110137
}
111138

112139
/*
@@ -123,7 +150,7 @@ public Value withId(Object id) {
123150
} else if (id.equals(_id)) {
124151
return this;
125152
}
126-
return new Value(id, _useInput);
153+
return new Value(id, _useInput, _optional);
127154
}
128155

129156
public Value withUseInput(Boolean useInput) {
@@ -134,7 +161,18 @@ public Value withUseInput(Boolean useInput) {
134161
} else if (useInput.equals(_useInput)) {
135162
return this;
136163
}
137-
return new Value(_id, useInput);
164+
return new Value(_id, useInput, _optional);
165+
}
166+
167+
public Value withOptional(Boolean optional) {
168+
if (optional == null) {
169+
if (_optional == null) {
170+
return this;
171+
}
172+
} else if (optional.equals(_optional)) {
173+
return this;
174+
}
175+
return new Value(_id, _useInput, optional);
138176
}
139177

140178
/*
@@ -145,6 +183,7 @@ public Value withUseInput(Boolean useInput) {
145183

146184
public Object getId() { return _id; }
147185
public Boolean getUseInput() { return _useInput; }
186+
public Boolean getOptional() { return _optional; }
148187

149188
public boolean hasId() {
150189
return _id != null;
@@ -162,8 +201,8 @@ public boolean willUseInput(boolean defaultSetting) {
162201

163202
@Override
164203
public String toString() {
165-
return String.format("JacksonInject.Value(id=%s,useInput=%s)",
166-
_id, _useInput);
204+
return String.format("JacksonInject.Value(id=%s,useInput=%s,optional=%s)",
205+
_id, _useInput, _optional);
167206
}
168207

169208
@Override
@@ -175,6 +214,9 @@ public int hashCode() {
175214
if (_useInput != null) {
176215
h += _useInput.hashCode();
177216
}
217+
if (_optional != null) {
218+
h += _optional.hashCode();
219+
}
178220
return h;
179221
}
180222

@@ -184,12 +226,13 @@ public boolean equals(Object o) {
184226
if (o == null) return false;
185227
if (o.getClass() == getClass()) {
186228
Value other = (Value) o;
187-
if (OptBoolean.equals(_useInput, other._useInput)) {
188-
if (_id == null) {
189-
return other._id == null;
190-
}
191-
return _id.equals(other._id);
192-
}
229+
230+
return (_id == null && other._id == null
231+
|| _id != null && _id.equals(other._id))
232+
&& (_useInput == null && other._useInput == null
233+
|| _useInput != null && _useInput.equals(other._useInput))
234+
&& (_optional == null && other._optional == null
235+
|| _optional != null && _optional.equals(other._optional));
193236
}
194237
return false;
195238
}
@@ -200,8 +243,8 @@ public boolean equals(Object o) {
200243
/**********************************************************
201244
*/
202245

203-
private static boolean _empty(Object id, Boolean useInput) {
204-
return (id == null) && (useInput == null);
246+
private static boolean _empty(Object id, Boolean useInput, Boolean optional) {
247+
return (id == null) && (useInput == null) && optional == null;
205248
}
206249
}
207250
}

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)