diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x
index 914a5f235e..81fd82fafc 100644
--- a/release-notes/VERSION-2.x
+++ b/release-notes/VERSION-2.x
@@ -8,6 +8,8 @@ Project: jackson-databind
#1467: Support `@JsonUnwrapped` with `@JsonCreator`
(implementation by Liam F)
+#2145: Add `JsonNode.optional(String name)` and `optional(int index)` methods
+ (fix by Joo-Hyuk K)
#2461: Nested `@JsonUnwrapped` property names not correctly handled
(reported by @plovell)
(fix contributed by @SandeepGaur2016)
diff --git a/src/main/java/com/fasterxml/jackson/databind/JsonNode.java b/src/main/java/com/fasterxml/jackson/databind/JsonNode.java
index bf90bb10c1..59c1b6b8c9 100644
--- a/src/main/java/com/fasterxml/jackson/databind/JsonNode.java
+++ b/src/main/java/com/fasterxml/jackson/databind/JsonNode.java
@@ -208,6 +208,53 @@ public boolean isObject() {
*/
@Override
public JsonNode get(String fieldName) { return null; }
+
+ /**
+ * Method for accessing value of the specified element of
+ * an array node, wrapped in an {@link Optional}. For other nodes,
+ * an empty Optional is always returned.
+ *
+ * For array nodes, index specifies
+ * exact location within array and allows for efficient iteration
+ * over child elements (underlying storage is guaranteed to
+ * be efficiently indexable, i.e. has random-access to elements).
+ * If index is less than 0, or equal-or-greater than
+ * node.size(), an empty Optional is returned; no exception is
+ * thrown for any index.
+ *
+ * NOTE: if the element value has been explicitly set as null
+ * (which is different from removal!),
+ * a {@link com.fasterxml.jackson.databind.node.NullNode} will be returned
+ * wrapped in an Optional, not an empty Optional.
+ *
+ * @return Optional containing the node that represents the value of the specified element,
+ * if this node is an array and has the specified element and otherwise, an
+ * empty Optional, never null.
+ *
+ * @since 2.19
+ */
+ public Optional optional(int index) { return Optional.empty(); }
+
+ /**
+ * Method for accessing value of the specified field of
+ * an object node. If this node is not an object (or it
+ * does not have a value for specified field name), or
+ * if there is no field with such name, empty {@link Optional}
+ * is returned.
+ *
+ * NOTE: if the property value has been explicitly set as null
+ * (which is different from removal!), an Optional containing
+ * {@link com.fasterxml.jackson.databind.node.NullNode} will be returned,
+ * not null.
+ *
+ * @return Optional that may contain value of the specified field,
+ * if this node is an object and has value for the specified
+ * field. Empty Optional otherwise never null.
+ *
+ * @since 2.19
+ */
+ public Optional optional(String propertyName) { return Optional.empty(); }
+
/**
* This method is similar to {@link #get(String)}, except
* that instead of returning null if no such value exists (due
diff --git a/src/main/java/com/fasterxml/jackson/databind/node/ArrayNode.java b/src/main/java/com/fasterxml/jackson/databind/node/ArrayNode.java
index c1bae6f931..7ba6df1a6a 100644
--- a/src/main/java/com/fasterxml/jackson/databind/node/ArrayNode.java
+++ b/src/main/java/com/fasterxml/jackson/databind/node/ArrayNode.java
@@ -269,6 +269,14 @@ public JsonNode get(int index) {
@Override
public JsonNode get(String fieldName) { return null; }
+ /**
+ * @since 2.19
+ */
+ @Override
+ public Optional optional(int index) {
+ return Optional.ofNullable(get(index));
+ }
+
@Override
public JsonNode path(String fieldName) { return MissingNode.getInstance(); }
diff --git a/src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java b/src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java
index 7df0f14d6a..58b936ee9a 100644
--- a/src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java
+++ b/src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java
@@ -283,9 +283,12 @@ public JsonNode get(String propertyName) {
return _children.get(propertyName);
}
+ /**
+ * @since 2.19
+ */
@Override
- public Iterator fieldNames() {
- return _children.keySet().iterator();
+ public Optional optional(String propertyName) {
+ return Optional.ofNullable(get(propertyName));
}
@Override
@@ -312,6 +315,11 @@ public JsonNode required(String propertyName) {
return _reportRequiredViolation("No value for property '%s' of `ObjectNode`", propertyName);
}
+ @Override
+ public Iterator fieldNames() {
+ return _children.keySet().iterator();
+ }
+
/**
* Method to use for accessing all properties (with both names
* and values) of this JSON Object.
diff --git a/src/test/java/com/fasterxml/jackson/databind/node/ArrayNodeTest.java b/src/test/java/com/fasterxml/jackson/databind/node/ArrayNodeTest.java
index dfcca31fde..9ce45d1663 100644
--- a/src/test/java/com/fasterxml/jackson/databind/node/ArrayNodeTest.java
+++ b/src/test/java/com/fasterxml/jackson/databind/node/ArrayNodeTest.java
@@ -48,6 +48,7 @@ public void testDirectCreation() throws Exception
assertFalse(n.fieldNames().hasNext());
assertNull(n.get("x")); // not used with arrays
assertTrue(n.path("x").isMissingNode());
+ assertFalse(n.optional("x").isPresent());
assertSame(text, n.get(0));
// single element, so:
@@ -439,12 +440,16 @@ public void testSimpleArray() throws Exception
// plus see that we can access stuff
assertEquals(NullNode.instance, result.path(0));
assertEquals(NullNode.instance, result.get(0));
+ assertEquals(NullNode.instance, result.optional(0).get());
assertEquals(BooleanNode.FALSE, result.path(1));
assertEquals(BooleanNode.FALSE, result.get(1));
+ assertEquals(BooleanNode.FALSE, result.optional(1).get());
assertEquals(2, result.size());
assertNull(result.get(-1));
assertNull(result.get(2));
+ assertFalse(result.optional(-1).isPresent());
+ assertFalse(result.optional(2).isPresent());
JsonNode missing = result.path(2);
assertTrue(missing.isMissingNode());
assertTrue(result.path(-100).isMissingNode());
diff --git a/src/test/java/com/fasterxml/jackson/databind/node/JsonNodeBasicTest.java b/src/test/java/com/fasterxml/jackson/databind/node/JsonNodeBasicTest.java
index f9be91e9a6..570998506f 100644
--- a/src/test/java/com/fasterxml/jackson/databind/node/JsonNodeBasicTest.java
+++ b/src/test/java/com/fasterxml/jackson/databind/node/JsonNodeBasicTest.java
@@ -217,6 +217,73 @@ public void testArrayWithDefaultTyping() throws Exception
assertEquals(2, obj.path("a").asInt());
}
+ // [databind#2145]
+ @Test
+ public void testOptionalAccessorOnArray() throws Exception {
+ ArrayNode arrayNode = MAPPER.createArrayNode();
+ arrayNode.add("firstElement");
+ assertTrue(arrayNode.optional(0).isPresent());
+ assertEquals("firstElement", arrayNode.optional(0).get().asText());
+ assertFalse(arrayNode.optional(1).isPresent());
+ assertFalse(arrayNode.optional(-1).isPresent());
+ assertFalse(arrayNode.optional(999).isPresent());
+ assertFalse(arrayNode.optional("anyField").isPresent());
+ }
+
+ @Test
+ public void testOptionalAccessorOnObject() throws Exception {
+ ObjectNode objectNode = MAPPER.createObjectNode();
+ objectNode.put("existingField", "value");
+ assertTrue(objectNode.optional("existingField").isPresent());
+ assertEquals("value", objectNode.optional("existingField").get().asText());
+ assertFalse(objectNode.optional("missingField").isPresent());
+ assertFalse(objectNode.optional(0).isPresent());
+ assertFalse(objectNode.optional(-1).isPresent());
+ }
+
+ @Test
+ public void testOptionalAccessorOnNumbers() throws Exception
+ {
+ // Test IntNode
+ IntNode intNode = IntNode.valueOf(42);
+ assertFalse(intNode.optional("anyField").isPresent());
+ assertFalse(intNode.optional(0).isPresent());
+
+ // Test LongNode
+ LongNode longNode = LongNode.valueOf(123456789L);
+ assertFalse(longNode.optional("anyField").isPresent());
+ assertFalse(longNode.optional(0).isPresent());
+
+ // Test DoubleNode
+ DoubleNode doubleNode = DoubleNode.valueOf(3.14);
+ assertFalse(doubleNode.optional("anyField").isPresent());
+ assertFalse(doubleNode.optional(0).isPresent());
+
+ // Test DecimalNode
+ DecimalNode decimalNode = DecimalNode.valueOf(new java.math.BigDecimal("12345.6789"));
+ assertFalse(decimalNode.optional("anyField").isPresent());
+ assertFalse(decimalNode.optional(0).isPresent());
+ }
+
+ @Test
+ public void testOptionalAccessorOnOtherTypes() throws Exception
+ {
+ // Test TextNode
+ TextNode textNode = TextNode.valueOf("sampleText");
+ assertFalse(textNode.optional("anyField").isPresent());
+ assertFalse(textNode.optional(0).isPresent());
+
+ // Test NullNode
+ NullNode nullNode = NullNode.getInstance();
+ assertFalse(nullNode.optional("anyField").isPresent());
+ assertFalse(nullNode.optional(0).isPresent());
+
+ // Test BooleanNode
+ BooleanNode booleanNode = BooleanNode.TRUE;
+ assertFalse(booleanNode.optional("anyField").isPresent());
+ assertFalse(booleanNode.optional(0).isPresent());
+ }
+
// [databind#4867]
@Test
public void testAsOptional() {
diff --git a/src/test/java/com/fasterxml/jackson/databind/node/ObjectNodeTest.java b/src/test/java/com/fasterxml/jackson/databind/node/ObjectNodeTest.java
index 7022c6f474..fa20121e5b 100644
--- a/src/test/java/com/fasterxml/jackson/databind/node/ObjectNodeTest.java
+++ b/src/test/java/com/fasterxml/jackson/databind/node/ObjectNodeTest.java
@@ -146,6 +146,7 @@ public void testBasics()
assertTrue(n.properties().isEmpty());
assertFalse(n.fieldNames().hasNext());
assertNull(n.get("a"));
+ assertFalse(n.optional("a").isPresent());
assertTrue(n.path("a").isMissingNode());
TextNode text = TextNode.valueOf("x");
@@ -249,6 +250,7 @@ public void testNullChecking()
JsonNode n = o1.get("x");
assertNotNull(n);
assertSame(n, NullNode.instance);
+ assertEquals(NullNode.instance, o1.optional("x").get());
o1.put("str", (String) null);
n = o1.get("str");