Skip to content

Add JsonNode.optional(String name) and optional(int index) methods #4866

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/main/java/com/fasterxml/jackson/databind/JsonNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*<p>
* 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
* <code>node.size()</code>, an empty Optional is returned; no exception is
* thrown for any index.
*<p>
* NOTE: if the element value has been explicitly set as <code>null</code>
* (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<JsonNode> 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.
*<p>
* NOTE: if the property value has been explicitly set as <code>null</code>
* (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<JsonNode> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,14 @@ public JsonNode get(int index) {
@Override
public JsonNode get(String fieldName) { return null; }

/**
* @since 2.19
*/
@Override
public Optional<JsonNode> optional(int index) {
return Optional.ofNullable(get(index));
}

@Override
public JsonNode path(String fieldName) { return MissingNode.getInstance(); }

Expand Down
12 changes: 10 additions & 2 deletions src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,12 @@ public JsonNode get(String propertyName) {
return _children.get(propertyName);
}

/**
* @since 2.19
*/
@Override
public Iterator<String> fieldNames() {
return _children.keySet().iterator();
public Optional<JsonNode> optional(String propertyName) {
return Optional.ofNullable(get(propertyName));
}

@Override
Expand All @@ -312,6 +315,11 @@ public JsonNode required(String propertyName) {
return _reportRequiredViolation("No value for property '%s' of `ObjectNode`", propertyName);
}

@Override
public Iterator<String> fieldNames() {
return _children.keySet().iterator();
}

/**
* Method to use for accessing all properties (with both names
* and values) of this JSON Object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down