From a4bf170e53e4d7b57c5e0d79727a3023ad88c7ff Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Wed, 30 Apr 2025 19:15:58 +0200 Subject: [PATCH 1/4] feat: jackson-databind#3072 values injected with JacksonInject can be optional now --- .../jackson/annotation/JacksonInject.java | 74 ++++++++++++++----- .../jackson/annotation/JacksonInjectTest.java | 45 +++++++++-- 2 files changed, 95 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java b/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java index c880b6c5..99fedc77 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java @@ -42,6 +42,19 @@ */ public OptBoolean useInput() default OptBoolean.DEFAULT; + /** + * Whether to throw an exception when the ObjectMapper doesn't find + * the injectable value. + *

+ * Default is `OptBoolean.FALSE` for backwards compatibility + * + * @return {@link OptBoolean#FALSE} to throw an exception; {@link OptBoolean#TRUE} + * to avoid throwing it. + * + * @since 2.20 + */ + public OptBoolean optional() default OptBoolean.FALSE; + /* /********************************************************** /* Value class used to enclose information, allow for @@ -63,7 +76,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 +86,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 +109,28 @@ public static Value empty() { return EMPTY; } - public static Value construct(Object id, Boolean useInput) { + 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); + if (optional == null) { + optional = false; + } + 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 +147,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 +158,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 +180,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 +198,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 +211,9 @@ public int hashCode() { if (_useInput != null) { h += _useInput.hashCode(); } + if (_optional != null) { + h += _optional.hashCode(); + } return h; } @@ -189,12 +223,14 @@ 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); - } + boolean idEquals = _id == null && other._id == null + || _id != null && _id.equals(other._id); + boolean useInputEquals = _useInput == null && other._useInput == null + || _useInput != null && _useInput.equals(other._useInput); + boolean optionalEquals = _optional == null && other._optional == null + || _optional != null && _optional.equals(other._optional); + + return idEquals && useInputEquals && optionalEquals; } return false; } @@ -205,8 +241,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..8d051af7 100644 --- a/src/test/java/com/fasterxml/jackson/annotation/JacksonInjectTest.java +++ b/src/test/java/com/fasterxml/jackson/annotation/JacksonInjectTest.java @@ -12,6 +12,9 @@ private final static class Bogus { @JacksonInject public int vanilla; + + @JacksonInject(optional = OptBoolean.TRUE) + public int optionalField; } private final JacksonInject.Value EMPTY = JacksonInject.Value.empty(); @@ -24,9 +27,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 +42,24 @@ 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, false), v, + "optional should be false by default"); + + JacksonInject optionalField = Bogus.class.getField("optionalField") + .getAnnotation(JacksonInject.class); + v = JacksonInject.Value.from(optionalField); + assertEquals(JacksonInject.Value.construct(null, null, true), v); } @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 +68,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 +103,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(); From 9ff6fc13c6dfb8b7087b8040691a3c8c6c24fcc0 Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Fri, 2 May 2025 12:23:43 +0200 Subject: [PATCH 2/4] fix(PR#291): applying minor changes as per PR#291 review --- .../jackson/annotation/JacksonInject.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java b/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java index 99fedc77..0ff67475 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java @@ -109,6 +109,14 @@ 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; @@ -116,9 +124,6 @@ public static Value construct(Object id, Boolean useInput, Boolean optional) { if (_empty(id, useInput, optional)) { return EMPTY; } - if (optional == null) { - optional = false; - } return new Value(id, useInput, optional); } @@ -223,14 +228,13 @@ public boolean equals(Object o) { if (o == null) return false; if (o.getClass() == getClass()) { Value other = (Value) o; - boolean idEquals = _id == null && other._id == null - || _id != null && _id.equals(other._id); - boolean useInputEquals = _useInput == null && other._useInput == null - || _useInput != null && _useInput.equals(other._useInput); - boolean optionalEquals = _optional == null && other._optional == null - || _optional != null && _optional.equals(other._optional); - - return idEquals && useInputEquals && optionalEquals; + + 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; } From c5e718b4983e1605f4ca1271e8e87df43d19bcca Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 2 May 2025 14:23:52 -0700 Subject: [PATCH 3/4] Minor tweaking --- release-notes/VERSION-2.x | 5 +++++ .../fasterxml/jackson/annotation/JacksonInject.java | 13 ++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 00d31564..5ad9cb08 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -11,6 +11,11 @@ NOTE: Annotations module will never contain changes in patch versions, === Releases === ------------------------------------------------------------------------ +2.20.0 (not yet released) + +#291: Add `optional` property for `@JacksonInject` to allow optionally injected values + (contributed by @giulong) + 2.19.0 (24-Apr-2025) #280: Minor change to `module-info.java`: use "open module" diff --git a/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java b/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java index 0ff67475..9ba991da 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JacksonInject.java @@ -43,17 +43,20 @@ public OptBoolean useInput() default OptBoolean.DEFAULT; /** - * Whether to throw an exception when the ObjectMapper doesn't find - * the injectable value. + * Whether to throw an exception when the {@code ObjectMapper} does not find + * the value to inject. *

- * Default is `OptBoolean.FALSE` for backwards compatibility + * 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. + * 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.FALSE; + public OptBoolean optional() default OptBoolean.DEFAULT; /* /********************************************************** From dfda4390f612d51dd8c9ff477a266adcdf4b470b Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 2 May 2025 15:18:37 -0700 Subject: [PATCH 4/4] Fix failing unit test --- .../fasterxml/jackson/annotation/JacksonInjectTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/fasterxml/jackson/annotation/JacksonInjectTest.java b/src/test/java/com/fasterxml/jackson/annotation/JacksonInjectTest.java index 8d051af7..51e0bf9d 100644 --- a/src/test/java/com/fasterxml/jackson/annotation/JacksonInjectTest.java +++ b/src/test/java/com/fasterxml/jackson/annotation/JacksonInjectTest.java @@ -7,7 +7,8 @@ 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 @@ -48,8 +49,8 @@ public void testFromAnnotation() throws Exception JacksonInject ann2 = Bogus.class.getField("vanilla").getAnnotation(JacksonInject.class); v = JacksonInject.Value.from(ann2); - assertEquals(JacksonInject.Value.construct(null, null, false), v, - "optional should be false by default"); + assertEquals(JacksonInject.Value.construct(null, null, null), v, + "optional should be `null` by default"); JacksonInject optionalField = Bogus.class.getField("optionalField") .getAnnotation(JacksonInject.class); @@ -57,6 +58,7 @@ public void testFromAnnotation() throws Exception assertEquals(JacksonInject.Value.construct(null, null, true), v); } + @SuppressWarnings("unlikely-arg-type") @Test public void testStdMethods() { assertEquals("JacksonInject.Value(id=null,useInput=null,optional=null)",