diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x
index 984627c6e7..a55162679f 100644
--- a/release-notes/VERSION-2.x
+++ b/release-notes/VERSION-2.x
@@ -72,6 +72,7 @@ Project: jackson-databind
trying to setAccessible on `OptionalInt` with JDK 17+
#4090: Support sequenced collections (JDK 21)S
(contributed by @pjfanning)
+#4095: Add `withObjectProperty(String)`, `withArrayProperty(String)` in `JsonNode`
#4122: Do not resolve wildcards if upper bound is too non-specific
(contributed by @yawkat)
diff --git a/src/main/java/com/fasterxml/jackson/databind/JsonNode.java b/src/main/java/com/fasterxml/jackson/databind/JsonNode.java
index 3d6878fea8..516d67bb6f 100644
--- a/src/main/java/com/fasterxml/jackson/databind/JsonNode.java
+++ b/src/main/java/com/fasterxml/jackson/databind/JsonNode.java
@@ -1261,6 +1261,36 @@ public ObjectNode withObject(JsonPointer ptr,
+getClass().getName()+"`");
}
+ /**
+ * Method similar to {@link #withObject(JsonPointer, OverwriteMode, boolean)} -- basically
+ * short-cut to:
+ *
+ * withObject(JsonPointer.compile("/"+propName), OverwriteMode.NULLS, false);
+ *
+ * that is, only matches immediate property on {@link ObjectNode}
+ * and will either use an existing {@link ObjectNode} that is
+ * value of the property, or create one if no value or value is {@code NullNode}.
+ *
+ * Will fail with an exception if:
+ *
+ * - Node method called on is NOT {@link ObjectNode}
+ *
+ * - Property has an existing value that is NOT {@code NullNode} (explicit {@code null})
+ *
+ *
+ *
+ * @param propName Name of property that has or will have {@link ObjectNode} as value
+ *
+ * @return {@link ObjectNode} value of given property (existing or created)
+ *
+ * @since 2.16
+ */
+ public ObjectNode withObjectProperty(String propName) {
+ // To avoid abstract method, base implementation just fails
+ throw new UnsupportedOperationException("`JsonNode` not of type `ObjectNode` (but `"
+ +getClass().getName()+")`, cannot call `withObjectProperty(String)` on it");
+ }
+
/**
* Method that works in one of possible ways, depending on whether
* {@code exprOrProperty} is a valid {@link JsonPointer} expression or
@@ -1409,6 +1439,36 @@ public ArrayNode withArray(JsonPointer ptr,
+getClass().getName());
}
+ /**
+ * Method similar to {@link #withArray(JsonPointer, OverwriteMode, boolean)} -- basically
+ * short-cut to:
+ *
+ * withArray(JsonPointer.compile("/"+propName), OverwriteMode.NULLS, false);
+ *
+ * that is, only matches immediate property on {@link ObjectNode}
+ * and will either use an existing {@link ArrayNode} that is
+ * value of the property, or create one if no value or value is {@code NullNode}.
+ *
+ * Will fail with an exception if:
+ *
+ * - Node method called on is NOT {@link ObjectNode}
+ *
+ * - Property has an existing value that is NOT {@code NullNode} (explicit {@code null})
+ *
+ *
+ *
+ * @param propName Name of property that has or will have {@link ArrayNode} as value
+ *
+ * @return {@link ArrayNode} value of given property (existing or created)
+ *
+ * @since 2.16
+ */
+ public ArrayNode withArrayProperty(String propName) {
+ // To avoid abstract method, base implementation just fails
+ throw new UnsupportedOperationException("`JsonNode` not of type `ObjectNode` (but `"
+ +getClass().getName()+")`, cannot call `withArrayProperty(String)` on it");
+ }
+
/*
/**********************************************************
/* Public API, comparison
diff --git a/src/main/java/com/fasterxml/jackson/databind/node/BaseJsonNode.java b/src/main/java/com/fasterxml/jackson/databind/node/BaseJsonNode.java
index 4650e2a425..508cb0e9f9 100644
--- a/src/main/java/com/fasterxml/jackson/databind/node/BaseJsonNode.java
+++ b/src/main/java/com/fasterxml/jackson/databind/node/BaseJsonNode.java
@@ -183,7 +183,7 @@ protected boolean _withXxxMayReplace(JsonNode node, OverwriteMode overwriteMode)
public ArrayNode withArray(JsonPointer ptr,
OverwriteMode overwriteMode, boolean preferIndex)
{
- // Degenerate case of using with "empty" path; ok if ObjectNode
+ // Degenerate case of using with "empty" path; ok if ArrayNode
if (ptr.matches()) {
if (this instanceof ArrayNode) {
return (ArrayNode) this;
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 35db7d578c..bb80be31f3 100644
--- a/src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java
+++ b/src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java
@@ -89,6 +89,20 @@ public ObjectNode with(String exprOrProperty) {
return result;
}
+ @Override
+ public ObjectNode withObjectProperty(String propName) {
+ JsonNode child = _children.get(propName);
+ if (child == null || child.isNull()) {
+ return putObject(propName);
+ }
+ if (child.isObject()) {
+ return (ObjectNode) child;
+ }
+ return _reportWrongNodeType(
+"Cannot replace `JsonNode` of type `%s` with `ObjectNode` for property \"%s\" (default mode `OverwriteMode.%s`)",
+child.getClass().getName(), propName, OverwriteMode.NULLS);
+ }
+
@SuppressWarnings("unchecked")
@Override
public ArrayNode withArray(String exprOrProperty)
@@ -111,6 +125,20 @@ public ArrayNode withArray(String exprOrProperty)
return result;
}
+ @Override
+ public ArrayNode withArrayProperty(String propName) {
+ JsonNode child = _children.get(propName);
+ if (child == null || child.isNull()) {
+ return putArray(propName);
+ }
+ if (child.isArray()) {
+ return (ArrayNode) child;
+ }
+ return _reportWrongNodeType(
+"Cannot replace `JsonNode` of type `%s` with `ArrayNode` for property \"%s\" with (default mode `OverwriteMode.%s`)",
+child.getClass().getName(), propName, OverwriteMode.NULLS);
+ }
+
@Override
protected ObjectNode _withObject(JsonPointer origPtr,
JsonPointer currentPtr,
diff --git a/src/test/java/com/fasterxml/jackson/databind/node/WithPathTest.java b/src/test/java/com/fasterxml/jackson/databind/node/WithPathTest.java
index 73cbf1e1e2..6b53a6687b 100644
--- a/src/test/java/com/fasterxml/jackson/databind/node/WithPathTest.java
+++ b/src/test/java/com/fasterxml/jackson/databind/node/WithPathTest.java
@@ -196,6 +196,47 @@ private void _verifyObjectReplaceFail(JsonNode doc, JsonPointer ptr, OverwriteMo
}
}
+ /*
+ /**********************************************************************
+ /* Test methods, withObjectProperty()
+ /**********************************************************************
+ */
+
+ public void testWithObjectProperty() throws Exception
+ {
+ ObjectNode root = MAPPER.createObjectNode();
+
+ // First: create new property value
+ ObjectNode match = root.withObjectProperty("a");
+ assertTrue(match.isObject());
+ assertEquals(a2q("{}"), match.toString());
+ match.put("value", 42);
+ assertEquals(a2q("{'a':{'value':42}}"), root.toString());
+
+ // Second: match existing Object property
+ ObjectNode match2 = root.withObjectProperty("a");
+ assertSame(match, match2);
+ match.put("value2", true);
+
+ assertEquals(a2q("{'a':{'value':42,'value2':true}}"),
+ root.toString());
+
+ // Third: match and overwrite existing null node
+ JsonNode root2 = MAPPER.readTree("{\"b\": null}");
+ ObjectNode match3 = root2.withObjectProperty("b");
+ assertNotSame(match, match3);
+ assertEquals("{\"b\":{}}", root2.toString());
+
+ // and then failing case
+ JsonNode root3 = MAPPER.readTree("{\"c\": 123}");
+ try {
+ root3.withObjectProperty("c");
+ fail("Should not pass");
+ } catch (UnsupportedOperationException e) {
+ verifyException(e, "Cannot replace `JsonNode` of type ");
+ }
+ }
+
/*
/**********************************************************************
/* Test methods, withArray()
@@ -315,4 +356,44 @@ public void testWithArray3882() throws Exception
assertEquals(a2q("{'key1':{'array1':[{'element1':['v1']}]}}"),
root.toString());
}
+
+ /*
+ /**********************************************************************
+ /* Test methods, withArrayProperty()
+ /**********************************************************************
+ */
+
+ public void testWithArrayProperty() throws Exception
+ {
+ ObjectNode root = MAPPER.createObjectNode();
+
+ // First: create new property value
+ ArrayNode match = root.withArrayProperty("a");
+ assertTrue(match.isArray());
+ assertEquals(a2q("[]"), match.toString());
+ match.add(42);
+ assertEquals(a2q("{'a':[42]}"), root.toString());
+
+ // Second: match existing Object property
+ ArrayNode match2 = root.withArrayProperty("a");
+ assertSame(match, match2);
+ match.add(true);
+
+ assertEquals(a2q("{'a':[42,true]}"), root.toString());
+
+ // Third: match and overwrite existing null node
+ JsonNode root2 = MAPPER.readTree("{\"b\": null}");
+ ArrayNode match3 = root2.withArrayProperty("b");
+ assertNotSame(match, match3);
+ assertEquals("{\"b\":[]}", root2.toString());
+
+ // and then failing case
+ JsonNode root3 = MAPPER.readTree("{\"c\": 123}");
+ try {
+ root3.withArrayProperty("c");
+ fail("Should not pass");
+ } catch (UnsupportedOperationException e) {
+ verifyException(e, "Cannot replace `JsonNode` of type ");
+ }
+ }
}