Skip to content

Commit 042cd3d

Browse files
authored
Fix #4095: add JsonNode.withObjectProperty()/.withArrayProperty() (#4131)
1 parent ebf2a82 commit 042cd3d

File tree

5 files changed

+171
-1
lines changed

5 files changed

+171
-1
lines changed

release-notes/VERSION-2.x

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Project: jackson-databind
7272
trying to setAccessible on `OptionalInt` with JDK 17+
7373
#4090: Support sequenced collections (JDK 21)S
7474
(contributed by @pjfanning)
75+
#4095: Add `withObjectProperty(String)`, `withArrayProperty(String)` in `JsonNode`
7576
#4122: Do not resolve wildcards if upper bound is too non-specific
7677
(contributed by @yawkat)
7778

src/main/java/com/fasterxml/jackson/databind/JsonNode.java

+60
Original file line numberDiff line numberDiff line change
@@ -1261,6 +1261,36 @@ public ObjectNode withObject(JsonPointer ptr,
12611261
+getClass().getName()+"`");
12621262
}
12631263

1264+
/**
1265+
* Method similar to {@link #withObject(JsonPointer, OverwriteMode, boolean)} -- basically
1266+
* short-cut to:
1267+
*<pre>
1268+
* withObject(JsonPointer.compile("/"+propName), OverwriteMode.NULLS, false);
1269+
*</pre>
1270+
* that is, only matches immediate property on {@link ObjectNode}
1271+
* and will either use an existing {@link ObjectNode} that is
1272+
* value of the property, or create one if no value or value is {@code NullNode}.
1273+
* <br>
1274+
* Will fail with an exception if:
1275+
* <ul>
1276+
* <li>Node method called on is NOT {@link ObjectNode}
1277+
* </li>
1278+
* <li>Property has an existing value that is NOT {@code NullNode} (explicit {@code null})
1279+
* </li>
1280+
* </ul>
1281+
*
1282+
* @param propName Name of property that has or will have {@link ObjectNode} as value
1283+
*
1284+
* @return {@link ObjectNode} value of given property (existing or created)
1285+
*
1286+
* @since 2.16
1287+
*/
1288+
public ObjectNode withObjectProperty(String propName) {
1289+
// To avoid abstract method, base implementation just fails
1290+
throw new UnsupportedOperationException("`JsonNode` not of type `ObjectNode` (but `"
1291+
+getClass().getName()+")`, cannot call `withObjectProperty(String)` on it");
1292+
}
1293+
12641294
/**
12651295
* Method that works in one of possible ways, depending on whether
12661296
* {@code exprOrProperty} is a valid {@link JsonPointer} expression or
@@ -1409,6 +1439,36 @@ public ArrayNode withArray(JsonPointer ptr,
14091439
+getClass().getName());
14101440
}
14111441

1442+
/**
1443+
* Method similar to {@link #withArray(JsonPointer, OverwriteMode, boolean)} -- basically
1444+
* short-cut to:
1445+
*<pre>
1446+
* withArray(JsonPointer.compile("/"+propName), OverwriteMode.NULLS, false);
1447+
*</pre>
1448+
* that is, only matches immediate property on {@link ObjectNode}
1449+
* and will either use an existing {@link ArrayNode} that is
1450+
* value of the property, or create one if no value or value is {@code NullNode}.
1451+
* <br>
1452+
* Will fail with an exception if:
1453+
* <ul>
1454+
* <li>Node method called on is NOT {@link ObjectNode}
1455+
* </li>
1456+
* <li>Property has an existing value that is NOT {@code NullNode} (explicit {@code null})
1457+
* </li>
1458+
* </ul>
1459+
*
1460+
* @param propName Name of property that has or will have {@link ArrayNode} as value
1461+
*
1462+
* @return {@link ArrayNode} value of given property (existing or created)
1463+
*
1464+
* @since 2.16
1465+
*/
1466+
public ArrayNode withArrayProperty(String propName) {
1467+
// To avoid abstract method, base implementation just fails
1468+
throw new UnsupportedOperationException("`JsonNode` not of type `ObjectNode` (but `"
1469+
+getClass().getName()+")`, cannot call `withArrayProperty(String)` on it");
1470+
}
1471+
14121472
/*
14131473
/**********************************************************
14141474
/* Public API, comparison

src/main/java/com/fasterxml/jackson/databind/node/BaseJsonNode.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ protected boolean _withXxxMayReplace(JsonNode node, OverwriteMode overwriteMode)
183183
public ArrayNode withArray(JsonPointer ptr,
184184
OverwriteMode overwriteMode, boolean preferIndex)
185185
{
186-
// Degenerate case of using with "empty" path; ok if ObjectNode
186+
// Degenerate case of using with "empty" path; ok if ArrayNode
187187
if (ptr.matches()) {
188188
if (this instanceof ArrayNode) {
189189
return (ArrayNode) this;

src/main/java/com/fasterxml/jackson/databind/node/ObjectNode.java

+28
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,20 @@ public ObjectNode with(String exprOrProperty) {
8989
return result;
9090
}
9191

92+
@Override
93+
public ObjectNode withObjectProperty(String propName) {
94+
JsonNode child = _children.get(propName);
95+
if (child == null || child.isNull()) {
96+
return putObject(propName);
97+
}
98+
if (child.isObject()) {
99+
return (ObjectNode) child;
100+
}
101+
return _reportWrongNodeType(
102+
"Cannot replace `JsonNode` of type `%s` with `ObjectNode` for property \"%s\" (default mode `OverwriteMode.%s`)",
103+
child.getClass().getName(), propName, OverwriteMode.NULLS);
104+
}
105+
92106
@SuppressWarnings("unchecked")
93107
@Override
94108
public ArrayNode withArray(String exprOrProperty)
@@ -111,6 +125,20 @@ public ArrayNode withArray(String exprOrProperty)
111125
return result;
112126
}
113127

128+
@Override
129+
public ArrayNode withArrayProperty(String propName) {
130+
JsonNode child = _children.get(propName);
131+
if (child == null || child.isNull()) {
132+
return putArray(propName);
133+
}
134+
if (child.isArray()) {
135+
return (ArrayNode) child;
136+
}
137+
return _reportWrongNodeType(
138+
"Cannot replace `JsonNode` of type `%s` with `ArrayNode` for property \"%s\" with (default mode `OverwriteMode.%s`)",
139+
child.getClass().getName(), propName, OverwriteMode.NULLS);
140+
}
141+
114142
@Override
115143
protected ObjectNode _withObject(JsonPointer origPtr,
116144
JsonPointer currentPtr,

src/test/java/com/fasterxml/jackson/databind/node/WithPathTest.java

+81
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,47 @@ private void _verifyObjectReplaceFail(JsonNode doc, JsonPointer ptr, OverwriteMo
196196
}
197197
}
198198

199+
/*
200+
/**********************************************************************
201+
/* Test methods, withObjectProperty()
202+
/**********************************************************************
203+
*/
204+
205+
public void testWithObjectProperty() throws Exception
206+
{
207+
ObjectNode root = MAPPER.createObjectNode();
208+
209+
// First: create new property value
210+
ObjectNode match = root.withObjectProperty("a");
211+
assertTrue(match.isObject());
212+
assertEquals(a2q("{}"), match.toString());
213+
match.put("value", 42);
214+
assertEquals(a2q("{'a':{'value':42}}"), root.toString());
215+
216+
// Second: match existing Object property
217+
ObjectNode match2 = root.withObjectProperty("a");
218+
assertSame(match, match2);
219+
match.put("value2", true);
220+
221+
assertEquals(a2q("{'a':{'value':42,'value2':true}}"),
222+
root.toString());
223+
224+
// Third: match and overwrite existing null node
225+
JsonNode root2 = MAPPER.readTree("{\"b\": null}");
226+
ObjectNode match3 = root2.withObjectProperty("b");
227+
assertNotSame(match, match3);
228+
assertEquals("{\"b\":{}}", root2.toString());
229+
230+
// and then failing case
231+
JsonNode root3 = MAPPER.readTree("{\"c\": 123}");
232+
try {
233+
root3.withObjectProperty("c");
234+
fail("Should not pass");
235+
} catch (UnsupportedOperationException e) {
236+
verifyException(e, "Cannot replace `JsonNode` of type ");
237+
}
238+
}
239+
199240
/*
200241
/**********************************************************************
201242
/* Test methods, withArray()
@@ -315,4 +356,44 @@ public void testWithArray3882() throws Exception
315356
assertEquals(a2q("{'key1':{'array1':[{'element1':['v1']}]}}"),
316357
root.toString());
317358
}
359+
360+
/*
361+
/**********************************************************************
362+
/* Test methods, withArrayProperty()
363+
/**********************************************************************
364+
*/
365+
366+
public void testWithArrayProperty() throws Exception
367+
{
368+
ObjectNode root = MAPPER.createObjectNode();
369+
370+
// First: create new property value
371+
ArrayNode match = root.withArrayProperty("a");
372+
assertTrue(match.isArray());
373+
assertEquals(a2q("[]"), match.toString());
374+
match.add(42);
375+
assertEquals(a2q("{'a':[42]}"), root.toString());
376+
377+
// Second: match existing Object property
378+
ArrayNode match2 = root.withArrayProperty("a");
379+
assertSame(match, match2);
380+
match.add(true);
381+
382+
assertEquals(a2q("{'a':[42,true]}"), root.toString());
383+
384+
// Third: match and overwrite existing null node
385+
JsonNode root2 = MAPPER.readTree("{\"b\": null}");
386+
ArrayNode match3 = root2.withArrayProperty("b");
387+
assertNotSame(match, match3);
388+
assertEquals("{\"b\":[]}", root2.toString());
389+
390+
// and then failing case
391+
JsonNode root3 = MAPPER.readTree("{\"c\": 123}");
392+
try {
393+
root3.withArrayProperty("c");
394+
fail("Should not pass");
395+
} catch (UnsupportedOperationException e) {
396+
verifyException(e, "Cannot replace `JsonNode` of type ");
397+
}
398+
}
318399
}

0 commit comments

Comments
 (0)