Skip to content

Commit 8f0b80d

Browse files
committed
Fix #2237
1 parent ad74329 commit 8f0b80d

File tree

10 files changed

+247
-5
lines changed

10 files changed

+247
-5
lines changed

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Project: jackson-databind
66

77
2.10.0.pr2 (not yet released)
88

9+
#2237: Add "required" methods in `JsonNode`: `required(String | int)`,
10+
`requiredAt(JsonPointer)`
911
#2331: `JsonMappingException` through nested getter with generic wildcard return type
1012
(reported by sunchezz89@github)
1113
#2336: `MapDeserializer` can not merge `Map`s with polymorphic values

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

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ public final boolean isBinary() {
404404
* @since 2.0
405405
*/
406406
public boolean canConvertToLong() { return false; }
407-
407+
408408
/*
409409
/**********************************************************
410410
/* Public API, straight value access
@@ -492,7 +492,7 @@ public byte[] binaryValue() throws IOException {
492492
* nodes.
493493
*/
494494
public long longValue() { return 0L; }
495-
495+
496496
/**
497497
* Returns 32-bit floating value for this node, <b>if and only if</b>
498498
* this node is numeric ({@link #isNumber} returns true). For other
@@ -676,6 +676,68 @@ public boolean asBoolean(boolean defaultValue) {
676676
return defaultValue;
677677
}
678678

679+
/*
680+
/**********************************************************************
681+
/* Public API, extended traversal (2.10) with "required()"
682+
/**********************************************************************
683+
*/
684+
685+
/**
686+
* @since 2.10
687+
*/
688+
public <T extends JsonNode> T require() {
689+
return _this();
690+
}
691+
692+
/**
693+
* @since 2.10
694+
*/
695+
public <T extends JsonNode> T requireNonNull() {
696+
return _this();
697+
}
698+
699+
/**
700+
* @since 2.10
701+
*/
702+
public JsonNode required(String fieldName) {
703+
return _reportRequiredViolation("Node of type `%s` has no fields", getClass().getName());
704+
}
705+
706+
/**
707+
* @since 2.10
708+
*/
709+
public JsonNode required(int index) {
710+
return _reportRequiredViolation("Node of type `%s` has no indexed values", getClass().getName());
711+
}
712+
713+
/**
714+
* @since 2.10
715+
*/
716+
public JsonNode requiredAt(String pathExpr) {
717+
return requiredAt(JsonPointer.compile(pathExpr));
718+
}
719+
720+
/**
721+
* @since 2.10
722+
*/
723+
public final JsonNode requiredAt(final JsonPointer pathExpr) {
724+
JsonPointer currentExpr = pathExpr;
725+
JsonNode curr = this;
726+
727+
// Note: copied from `at()`
728+
while (true) {
729+
if (currentExpr.matches()) {
730+
return curr;
731+
}
732+
curr = curr._at(currentExpr);
733+
if (curr == null) {
734+
_reportRequiredViolation("No node at '%s' (unmatched part: '%s')",
735+
pathExpr, currentExpr);
736+
}
737+
currentExpr = currentExpr.tail();
738+
}
739+
}
740+
679741
/*
680742
/**********************************************************
681743
/* Public API, value find / existence check methods
@@ -1000,4 +1062,25 @@ public String toPrettyString() {
10001062
*/
10011063
@Override
10021064
public abstract boolean equals(Object o);
1065+
1066+
/*
1067+
/**********************************************************************
1068+
/* Helper methods, for sub-classes
1069+
/**********************************************************************
1070+
*/
1071+
1072+
// @since 2.10
1073+
@SuppressWarnings("unchecked")
1074+
protected <T extends JsonNode> T _this() {
1075+
return (T) this;
1076+
}
1077+
1078+
/**
1079+
* Helper method that throws {@link IllegalArgumentException} as a result of
1080+
* violating "required-constraint" for this node (for {@link #require() or related
1081+
* methods).
1082+
*/
1083+
protected <T> T _reportRequiredViolation(String msgTemplate, Object...args) {
1084+
throw new IllegalArgumentException(String.format(msgTemplate, args));
1085+
}
10031086
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,15 @@ public JsonNode path(int index) {
132132
return MissingNode.getInstance();
133133
}
134134

135+
@Override
136+
public JsonNode required(int index) {
137+
if ((index >= 0) && (index < _children.size())) {
138+
return _children.get(index);
139+
}
140+
return _reportRequiredViolation("No value at index #%d [0, %d) of `ArrayNode`",
141+
index, _children.size());
142+
}
143+
135144
@Override
136145
public boolean equals(Comparator<JsonNode> comparator, JsonNode o)
137146
{

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ public final JsonNode findPath(String fieldName)
4949
// Also, force (re)definition (2.7)
5050
@Override public abstract int hashCode();
5151

52+
/*
53+
/**********************************************************************
54+
/* Improved required-ness checks for standard JsonNode implementations
55+
/**********************************************************************
56+
*/
57+
58+
@Override
59+
public JsonNode required(String fieldName) {
60+
return _reportRequiredViolation("Node of type `%s` has no fields",
61+
getClass().getSimpleName());
62+
}
63+
64+
@Override
65+
public JsonNode required(int index) {
66+
return _reportRequiredViolation("Node of type `%s` has no indexed values",
67+
getClass().getSimpleName());
68+
}
69+
5270
/*
5371
/**********************************************************
5472
/* Support for traversal-as-stream

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ public class BigIntegerNode
1717
private final static BigInteger MAX_INTEGER = BigInteger.valueOf(Integer.MAX_VALUE);
1818
private final static BigInteger MIN_LONG = BigInteger.valueOf(Long.MIN_VALUE);
1919
private final static BigInteger MAX_LONG = BigInteger.valueOf(Long.MAX_VALUE);
20-
20+
2121
final protected BigInteger _value;
22-
22+
2323
/*
2424
/**********************************************************
2525
/* Construction

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,18 @@ public boolean equals(Object o)
106106
return (o == this);
107107
}
108108

109+
@SuppressWarnings("unchecked")
110+
@Override
111+
public JsonNode require() {
112+
return _reportRequiredViolation("require() called on `MissingNode`");
113+
}
114+
115+
@SuppressWarnings("unchecked")
116+
@Override
117+
public JsonNode requireNonNull() {
118+
return _reportRequiredViolation("requireNonNull() called on `MissingNode`");
119+
}
120+
109121
@Override
110122
public int hashCode() {
111123
return JsonNodeType.MISSING.ordinal();

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.io.IOException;
44

55
import com.fasterxml.jackson.core.*;
6+
import com.fasterxml.jackson.databind.JsonNode;
67
import com.fasterxml.jackson.databind.SerializerProvider;
78

89

@@ -42,6 +43,12 @@ public JsonNodeType getNodeType() {
4243
@Override public String asText(String defaultValue) { return defaultValue; }
4344
@Override public String asText() { return "null"; }
4445

46+
@SuppressWarnings("unchecked")
47+
@Override
48+
public JsonNode requireNonNull() {
49+
return _reportRequiredViolation("requireNonNull() called on `NullNode`");
50+
}
51+
4552
// as with MissingNode, not considered number node; hence defaults are returned if provided
4653

4754
/*

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,15 @@ public JsonNode path(String fieldName)
129129
return MissingNode.getInstance();
130130
}
131131

132+
@Override
133+
public JsonNode required(String fieldName) {
134+
JsonNode n = _children.get(fieldName);
135+
if (n != null) {
136+
return n;
137+
}
138+
return _reportRequiredViolation("No value for property '%s' of `ObjectNode`", fieldName);
139+
}
140+
132141
/**
133142
* Method to use for accessing all fields (with both names
134143
* and values) of this JSON Object.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ static class MyValue
6868
/**********************************************************
6969
*/
7070

71-
private final ObjectMapper MAPPER = objectMapper();
71+
private final ObjectMapper MAPPER = sharedMapper();
7272

7373
public void testSimpleObject() throws Exception
7474
{
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package com.fasterxml.jackson.databind.node;
2+
3+
import com.fasterxml.jackson.core.JsonPointer;
4+
import com.fasterxml.jackson.databind.BaseMapTest;
5+
import com.fasterxml.jackson.databind.JsonNode;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
8+
public class RequiredAccessorTest
9+
extends BaseMapTest
10+
{
11+
private final ObjectMapper MAPPER = sharedMapper();
12+
13+
private final JsonNode TEST_OBJECT, TEST_ARRAY;
14+
15+
public RequiredAccessorTest() throws Exception {
16+
TEST_OBJECT = MAPPER.readTree(aposToQuotes(
17+
"{ 'data' : { 'primary' : 15, 'vector' : [ 'yes', false ], 'nullable' : null },\n"
18+
+" 'array' : [ true, {\"messsage\":'hello', 'value' : 42, 'misc' : [1, 2] }, null, 0.25 ]\n"
19+
+"}"
20+
));
21+
TEST_ARRAY = MAPPER.readTree(aposToQuotes(
22+
"[ true, { 'data' : { 'primary' : 15, 'vector' : [ 'yes', false ] } }, 0.25, 'last' ]"
23+
));
24+
}
25+
26+
public void testIMPORTANT() {
27+
_checkRequiredAt(TEST_OBJECT, "/data/weird/and/more", "/weird/and/more");
28+
}
29+
30+
public void testRequiredAtObjectOk() throws Exception {
31+
assertNotNull(TEST_OBJECT.requiredAt("/array"));
32+
assertNotNull(TEST_OBJECT.requiredAt("/array/0"));
33+
assertTrue(TEST_OBJECT.requiredAt("/array/0").isBoolean());
34+
assertNotNull(TEST_OBJECT.requiredAt("/array/1/misc/1"));
35+
assertEquals(2, TEST_OBJECT.requiredAt("/array/1/misc/1").intValue());
36+
}
37+
38+
public void testRequiredAtArrayOk() throws Exception {
39+
assertTrue(TEST_ARRAY.requiredAt("/0").isBoolean());
40+
assertTrue(TEST_ARRAY.requiredAt("/1").isObject());
41+
assertNotNull(TEST_ARRAY.requiredAt("/1/data/primary"));
42+
assertNotNull(TEST_ARRAY.requiredAt("/1/data/vector/1"));
43+
}
44+
45+
public void testRequiredAtFailOnObject() throws Exception {
46+
_checkRequiredAt(TEST_OBJECT, "/0", "/0");
47+
_checkRequiredAt(TEST_OBJECT, "/bogus", "/bogus");
48+
_checkRequiredAt(TEST_OBJECT, "/data/weird/and/more", "/weird/and/more");
49+
50+
_checkRequiredAt(TEST_OBJECT, "/data/vector/other/3", "/other/3");
51+
}
52+
53+
public void testRequiredAtFailOnArray() throws Exception {
54+
_checkRequiredAt(TEST_ARRAY, "/1/data/vector/25", "/25");
55+
}
56+
57+
private void _checkRequiredAt(JsonNode doc, String fullPath, String mismatchPart) {
58+
try {
59+
doc.requiredAt(fullPath);
60+
} catch (IllegalArgumentException e) {
61+
verifyException(e, "No node at '"+fullPath+"' (unmatched part: '"+mismatchPart+"')");
62+
}
63+
}
64+
65+
public void testSimpleRequireOk() throws Exception {
66+
// first basic working accessors on node itself
67+
assertSame(TEST_OBJECT, TEST_OBJECT.require());
68+
assertSame(TEST_OBJECT, TEST_OBJECT.requireNonNull());
69+
assertSame(TEST_OBJECT, TEST_OBJECT.requiredAt(""));
70+
assertSame(TEST_OBJECT, TEST_OBJECT.requiredAt(JsonPointer.compile("")));
71+
72+
assertSame(TEST_OBJECT.get("data"), TEST_OBJECT.required("data"));
73+
assertSame(TEST_ARRAY.get(0), TEST_ARRAY.required(0));
74+
assertSame(TEST_ARRAY.get(3), TEST_ARRAY.required(3));
75+
76+
// check diff between missing, null nodes
77+
TEST_OBJECT.path("data").path("nullable").require();
78+
79+
try {
80+
JsonNode n = TEST_OBJECT.path("data").path("nullable").requireNonNull();
81+
fail("Should not pass; got: "+n);
82+
} catch (IllegalArgumentException e) {
83+
verifyException(e, "requireNonNull() called on `NullNode`");
84+
}
85+
}
86+
87+
public void testSimpleRequireFail() throws Exception {
88+
try {
89+
TEST_OBJECT.required("bogus");
90+
fail("Should not pass");
91+
} catch (IllegalArgumentException e) {
92+
verifyException(e, "No value for property 'bogus'");
93+
}
94+
95+
try {
96+
TEST_ARRAY.required("bogus");
97+
fail("Should not pass");
98+
} catch (IllegalArgumentException e) {
99+
verifyException(e, "Node of type `ArrayNode` has no fields");
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)