Skip to content

Commit 83675a3

Browse files
committed
Fix #4248: add special handling for null "cause" for Throwable deserialization
1 parent 26f232a commit 83675a3

File tree

4 files changed

+63
-13
lines changed

4 files changed

+63
-13
lines changed

release-notes/VERSION-2.x

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Project: jackson-databind
1919
#4214: `EnumSet` deserialization does not work when we activate
2020
default typing in `ObjectMapper`
2121
(reported by @dvhvsekhar)
22+
#4248: `ThrowableDeserializer` does not handle `null` well for `cause`
2223

2324
2.16.1 (not yet released)
2425

src/main/java/com/fasterxml/jackson/databind/deser/std/ThrowableDeserializer.java

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.fasterxml.jackson.databind.deser.std;
22

33
import java.io.IOException;
4+
import java.util.Arrays;
45

56
import com.fasterxml.jackson.core.*;
67

@@ -97,18 +98,22 @@ public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) t
9798
return ctxt.handleMissingInstantiator(handledType(), getValueInstantiator(), p,
9899
"Throwable needs a default constructor, a single-String-arg constructor; or explicit @JsonCreator");
99100
}
100-
101101
Throwable throwable = null;
102102
Object[] pending = null;
103103
Throwable[] suppressed = null;
104104
int pendingIx = 0;
105-
106105
for (; !p.hasToken(JsonToken.END_OBJECT); p.nextToken()) {
107106
String propName = p.currentName();
108107
SettableBeanProperty prop = _beanProperties.find(propName);
109108
p.nextToken(); // to point to field value
110109

111110
if (prop != null) { // normal case
111+
// 07-Dec-2023, tatu: [databind#4248] Interesting that "cause"
112+
// with `null` blows up. So, avoid.
113+
if ("cause".equals(prop.getName())
114+
&& p.hasToken(JsonToken.VALUE_NULL)) {
115+
continue;
116+
}
112117
if (throwable != null) {
113118
prop.deserializeAndSet(p, ctxt, throwable);
114119
continue;
@@ -117,6 +122,13 @@ public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) t
117122
if (pending == null) {
118123
int len = _beanProperties.size();
119124
pending = new Object[len + len];
125+
} else if (pendingIx == pending.length) {
126+
// NOTE: only occurs with duplicate properties, possible
127+
// with some formats (most notably XML; but possibly with
128+
// JSON if duplicate detection not enabled). Most likely
129+
// only occurs with malicious content so use linear buffer
130+
// resize (no need to optimize performance)
131+
pending = Arrays.copyOf(pending, pendingIx + 16);
120132
}
121133
pending[pendingIx++] = prop;
122134
pending[pendingIx++] = prop.deserialize(p, ctxt);
@@ -142,7 +154,13 @@ public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) t
142154
continue;
143155
}
144156
if (PROP_NAME_SUPPRESSED.equalsIgnoreCase(propName)) { // or "suppressed"?
145-
suppressed = ctxt.readValue(p, Throwable[].class);
157+
// 07-Dec-2023, tatu: Not sure how/why, but JSON Null is otherwise
158+
// not handled with such call so...
159+
if (p.hasToken(JsonToken.VALUE_NULL)) {
160+
suppressed = null;
161+
} else {
162+
suppressed = ctxt.readValue(p, Throwable[].class);
163+
}
146164
continue;
147165
}
148166
if (PROP_NAME_LOCALIZED_MESSAGE.equalsIgnoreCase(propName)) {

src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java

+11
Original file line numberDiff line numberDiff line change
@@ -693,8 +693,19 @@ public static String getTypeDescription(JavaType fullType)
693693
if (fullType == null) {
694694
return "[null]";
695695
}
696+
// 07-Dec-2023, tatu: Instead of cryptic notation for array types
697+
// (JLS-specified for JDK deserialization), let's use trailing "[]"s
698+
// to indicate dimensions instead
699+
int arrays = 0;
700+
while (fullType.isArrayType()) {
701+
++arrays;
702+
fullType = fullType.getContentType();
703+
}
696704
StringBuilder sb = new StringBuilder(80).append('`');
697705
sb.append(fullType.toCanonical());
706+
while (arrays-- > 0) {
707+
sb.append("[]");
708+
}
698709
return sb.append('`').toString();
699710
}
700711

src/test/java/com/fasterxml/jackson/databind/exc/ExceptionDeserializationTest.java

+30-10
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ static class MyNoArgException extends Exception
5656

5757
private final ObjectMapper MAPPER = new ObjectMapper();
5858

59-
public void testIOException() throws IOException
59+
public void testIOException() throws Exception
6060
{
6161
IOException ioe = new IOException("TEST");
6262
String json = MAPPER.writerWithDefaultPrettyPrinter()
@@ -65,7 +65,7 @@ public void testIOException() throws IOException
6565
assertEquals(ioe.getMessage(), result.getMessage());
6666
}
6767

68-
public void testWithCreator() throws IOException
68+
public void testWithCreator() throws Exception
6969
{
7070
final String MSG = "the message";
7171
String json = MAPPER.writeValueAsString(new MyException(MSG, 3));
@@ -82,7 +82,7 @@ public void testWithCreator() throws IOException
8282
assertTrue(result.stuff.containsKey("suppressed"));
8383
}
8484

85-
public void testWithNullMessage() throws IOException
85+
public void testWithNullMessage() throws Exception
8686
{
8787
final ObjectMapper mapper = new ObjectMapper();
8888
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
@@ -92,14 +92,14 @@ public void testWithNullMessage() throws IOException
9292
assertNull(result.getMessage());
9393
}
9494

95-
public void testNoArgsException() throws IOException
95+
public void testNoArgsException() throws Exception
9696
{
9797
MyNoArgException exc = MAPPER.readValue("{}", MyNoArgException.class);
9898
assertNotNull(exc);
9999
}
100100

101101
// try simulating JDK 7 behavior
102-
public void testJDK7SuppressionProperty() throws IOException
102+
public void testJDK7SuppressionProperty() throws Exception
103103
{
104104
Exception exc = MAPPER.readValue("{\"suppressed\":[]}", IOException.class);
105105
assertNotNull(exc);
@@ -124,7 +124,7 @@ public void testSingleValueArrayDeserialization() throws Exception
124124
_assertEquality(exp.getStackTrace(), cloned.getStackTrace());
125125
}
126126

127-
public void testExceptionCauseDeserialization() throws IOException
127+
public void testExceptionCauseDeserialization() throws Exception
128128
{
129129
ObjectMapper mapper = new ObjectMapper();
130130

@@ -139,7 +139,7 @@ public void testExceptionCauseDeserialization() throws IOException
139139
}
140140

141141

142-
public void testSuppressedGenericThrowableDeserialization() throws IOException
142+
public void testSuppressedGenericThrowableDeserialization() throws Exception
143143
{
144144
ObjectMapper mapper = new ObjectMapper();
145145

@@ -155,7 +155,7 @@ public void testSuppressedGenericThrowableDeserialization() throws IOException
155155
_assertEquality(exp.getSuppressed()[0].getStackTrace(), act.getSuppressed()[0].getStackTrace());
156156
}
157157

158-
public void testSuppressedTypedExceptionDeserialization() throws IOException
158+
public void testSuppressedTypedExceptionDeserialization() throws Exception
159159
{
160160
PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder()
161161
.allowIfSubTypeIsArray()
@@ -231,7 +231,7 @@ public void testSingleValueArrayDeserializationException() throws Exception {
231231
}
232232

233233
// mostly to help with XML module (and perhaps CSV)
234-
public void testLineNumberAsString() throws IOException
234+
public void testLineNumberAsString() throws Exception
235235
{
236236
Exception exc = MAPPER.readValue(a2q(
237237
"{'message':'Test',\n'stackTrace': "
@@ -241,7 +241,7 @@ public void testLineNumberAsString() throws IOException
241241
}
242242

243243
// [databind#1842]
244-
public void testNullAsMessage() throws IOException
244+
public void testNullAsMessage() throws Exception
245245
{
246246
Exception exc = MAPPER.readValue(a2q(
247247
"{'message':null, 'localizedMessage':null }"
@@ -278,4 +278,24 @@ private void _testRoundtripWith(ObjectMapper mapper) throws Exception
278278
assertNotNull(result.getCause());
279279
assertEquals(root.getMessage(), result.getCause().getMessage());
280280
}
281+
282+
// [databind#4248]
283+
public void testWithDups() throws Exception
284+
{
285+
// NOTE: by default JSON parser does NOT fail on duplicate properties;
286+
// we only use them to mimic formats like XML where duplicates can occur
287+
// (or, malicious JSON...)
288+
final StringBuilder sb = new StringBuilder(100);
289+
sb.append("{");
290+
sb.append("'suppressed': [],\n");
291+
sb.append("'cause': null,\n");
292+
for (int i = 0; i < 10; ++i) { // just needs to be more than max distinct props
293+
sb.append("'stackTrace': [],\n");
294+
}
295+
sb.append("'message': 'foo',\n");
296+
sb.append("'localizedMessage': 'bar'\n}");
297+
IOException exc = MAPPER.readValue(a2q(sb.toString()), IOException.class);
298+
assertNotNull(exc);
299+
assertEquals("foo", exc.getLocalizedMessage());
300+
}
281301
}

0 commit comments

Comments
 (0)