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");