diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 584d0b6d..84f76ba7 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -13,6 +13,8 @@ NOTE: Annotations module will never contain changes in patch versions, 2.20.0 (not yet released) +#291: Add `optional` property for `@JacksonInject` to allow optionally injected values + (contributed by @giulong) - Generate SBOMs [JSTEP-14] 2.19.0 (24-Apr-2025) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java b/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java index c880b6c5..9ba991da 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java @@ -42,6 +42,22 @@ */ public OptBoolean useInput() default OptBoolean.DEFAULT; + /** + * Whether to throw an exception when the {@code ObjectMapper} does not find + * the value to inject. + *
+ * Default is {@code OptBoolean.DEFAULT} for backwards-compatibility: in this + * case {@code ObjectMapper} defaults are used (which in turn are same + * as {code OptBoolean.FALSE}). + * + * @return {@link OptBoolean#FALSE} to throw an exception; {@link OptBoolean#TRUE} + * to avoid throwing it; or {@link OptBoolean#DEFAULT} to use configure defaults + * (which are same as {@link OptBoolean#FALSE} for Jackson 2.x) + * + * @since 2.20 + */ + public OptBoolean optional() default OptBoolean.DEFAULT; + /* /********************************************************** /* Value class used to enclose information, allow for @@ -63,7 +79,7 @@ public static class Value { private static final long serialVersionUID = 1L; - protected final static Value EMPTY = new Value(null, null); + protected final static Value EMPTY = new Value(null, null, null); /** * Id to use to access injected value; if `null`, "default" name, derived @@ -73,9 +89,12 @@ public static class Value protected final Boolean _useInput; - protected Value(Object id, Boolean useInput) { + protected final Boolean _optional; + + protected Value(Object id, Boolean useInput, Boolean optional) { _id = id; _useInput = useInput; + _optional = optional; } @Override @@ -93,25 +112,33 @@ public static Value empty() { return EMPTY; } + @Deprecated //since 2.20 public static Value construct(Object id, Boolean useInput) { + return construct(id, useInput, null); + } + + /** + * @since 2.20 + */ + public static Value construct(Object id, Boolean useInput, Boolean optional) { if ("".equals(id)) { id = null; } - if (_empty(id, useInput)) { + if (_empty(id, useInput, optional)) { return EMPTY; } - return new Value(id, useInput); + return new Value(id, useInput, optional); } public static Value from(JacksonInject src) { if (src == null) { return EMPTY; } - return construct(src.value(), src.useInput().asBoolean()); + return construct(src.value(), src.useInput().asBoolean(), src.optional().asBoolean()); } public static Value forId(Object id) { - return construct(id, null); + return construct(id, null, null); } /* @@ -128,7 +155,7 @@ public Value withId(Object id) { } else if (id.equals(_id)) { return this; } - return new Value(id, _useInput); + return new Value(id, _useInput, _optional); } public Value withUseInput(Boolean useInput) { @@ -139,7 +166,18 @@ public Value withUseInput(Boolean useInput) { } else if (useInput.equals(_useInput)) { return this; } - return new Value(_id, useInput); + return new Value(_id, useInput, _optional); + } + + public Value withOptional(Boolean optional) { + if (optional == null) { + if (_optional == null) { + return this; + } + } else if (optional.equals(_optional)) { + return this; + } + return new Value(_id, _useInput, optional); } /* @@ -150,6 +188,7 @@ public Value withUseInput(Boolean useInput) { public Object getId() { return _id; } public Boolean getUseInput() { return _useInput; } + public Boolean getOptional() { return _optional; } public boolean hasId() { return _id != null; @@ -167,8 +206,8 @@ public boolean willUseInput(boolean defaultSetting) { @Override public String toString() { - return String.format("JacksonInject.Value(id=%s,useInput=%s)", - _id, _useInput); + return String.format("JacksonInject.Value(id=%s,useInput=%s,optional=%s)", + _id, _useInput, _optional); } @Override @@ -180,6 +219,9 @@ public int hashCode() { if (_useInput != null) { h += _useInput.hashCode(); } + if (_optional != null) { + h += _optional.hashCode(); + } return h; } @@ -189,12 +231,13 @@ public boolean equals(Object o) { if (o == null) return false; if (o.getClass() == getClass()) { Value other = (Value) o; - if (OptBoolean.equals(_useInput, other._useInput)) { - if (_id == null) { - return other._id == null; - } - return _id.equals(other._id); - } + + return (_id == null && other._id == null + || _id != null && _id.equals(other._id)) + && (_useInput == null && other._useInput == null + || _useInput != null && _useInput.equals(other._useInput)) + && (_optional == null && other._optional == null + || _optional != null && _optional.equals(other._optional)); } return false; } @@ -205,8 +248,8 @@ public boolean equals(Object o) { /********************************************************** */ - private static boolean _empty(Object id, Boolean useInput) { - return (id == null) && (useInput == null); + private static boolean _empty(Object id, Boolean useInput, Boolean optional) { + return (id == null) && (useInput == null) && optional == null; } } } diff --git a/src/test/java/com/fasterxml/jackson/annotation/JacksonInjectTest.java b/src/test/java/com/fasterxml/jackson/annotation/JacksonInjectTest.java index 3028d97c..51e0bf9d 100644 --- a/src/test/java/com/fasterxml/jackson/annotation/JacksonInjectTest.java +++ b/src/test/java/com/fasterxml/jackson/annotation/JacksonInjectTest.java @@ -7,11 +7,15 @@ public class JacksonInjectTest { private final static class Bogus { - @JacksonInject(value="inject", useInput=OptBoolean.FALSE) + @JacksonInject(value="inject", useInput=OptBoolean.FALSE, + optional=OptBoolean.FALSE) public int field; @JacksonInject public int vanilla; + + @JacksonInject(optional = OptBoolean.TRUE) + public int optionalField; } private final JacksonInject.Value EMPTY = JacksonInject.Value.empty(); @@ -24,9 +28,9 @@ public void testEmpty() assertTrue(EMPTY.willUseInput(true)); assertFalse(EMPTY.willUseInput(false)); - assertSame(EMPTY, JacksonInject.Value.construct(null, null)); + assertSame(EMPTY, JacksonInject.Value.construct(null, null, null)); // also, "" gets coerced to null so - assertSame(EMPTY, JacksonInject.Value.construct("", null)); + assertSame(EMPTY, JacksonInject.Value.construct("", null, null)); } @Test @@ -39,18 +43,25 @@ public void testFromAnnotation() throws Exception assertEquals("inject", v.getId()); assertEquals(Boolean.FALSE, v.getUseInput()); - assertEquals("JacksonInject.Value(id=inject,useInput=false)", v.toString()); + assertEquals("JacksonInject.Value(id=inject,useInput=false,optional=false)", v.toString()); assertFalse(v.equals(EMPTY)); assertFalse(EMPTY.equals(v)); JacksonInject ann2 = Bogus.class.getField("vanilla").getAnnotation(JacksonInject.class); v = JacksonInject.Value.from(ann2); - assertSame(EMPTY, v); + assertEquals(JacksonInject.Value.construct(null, null, null), v, + "optional should be `null` by default"); + + JacksonInject optionalField = Bogus.class.getField("optionalField") + .getAnnotation(JacksonInject.class); + v = JacksonInject.Value.from(optionalField); + assertEquals(JacksonInject.Value.construct(null, null, true), v); } + @SuppressWarnings("unlikely-arg-type") @Test public void testStdMethods() { - assertEquals("JacksonInject.Value(id=null,useInput=null)", + assertEquals("JacksonInject.Value(id=null,useInput=null,optional=null)", EMPTY.toString()); int x = EMPTY.hashCode(); if (x == 0) { // no fixed value, but should not evaluate to 0 @@ -59,6 +70,25 @@ public void testStdMethods() { assertEquals(EMPTY, EMPTY); assertFalse(EMPTY.equals(null)); assertFalse(EMPTY.equals("xyz")); + + JacksonInject.Value equals1 = JacksonInject.Value.construct("value", true, true); + JacksonInject.Value equals2 = JacksonInject.Value.construct("value", true, true); + JacksonInject.Value valueNull = JacksonInject.Value.construct(null, true, true); + JacksonInject.Value useInputNull = JacksonInject.Value.construct("value", null, true); + JacksonInject.Value optionalNull = JacksonInject.Value.construct("value", true, null); + JacksonInject.Value valueNotEqual = JacksonInject.Value.construct("not equal", true, true); + JacksonInject.Value useInputNotEqual = JacksonInject.Value.construct("value", false, true); + JacksonInject.Value optionalNotEqual = JacksonInject.Value.construct("value", true, false); + String string = "string"; + + assertEquals(equals1, equals2); + assertNotEquals(equals1, valueNull); + assertNotEquals(equals1, useInputNull); + assertNotEquals(equals1, optionalNull); + assertNotEquals(equals1, valueNotEqual); + assertNotEquals(equals1, useInputNotEqual); + assertNotEquals(equals1, optionalNotEqual); + assertNotEquals(equals1, string); } @Test @@ -75,6 +105,13 @@ public void testFactories() throws Exception assertFalse(v2.equals(v)); assertSame(v2, v2.withUseInput(Boolean.TRUE)); + JacksonInject.Value v3 = v.withOptional(Boolean.TRUE); + assertNotSame(v, v3); + assertFalse(v.equals(v3)); + assertFalse(v3.equals(v)); + assertSame(v3, v3.withOptional(Boolean.TRUE)); + assertTrue(v3.getOptional()); + int x = v2.hashCode(); if (x == 0) { // no fixed value, but should not evaluate to 0 fail();