Skip to content

Fix #4248: add special handling for null "cause" for Throwable deserialization #4249

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 1 commit into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Project: jackson-databind
#4214: `EnumSet` deserialization does not work when we activate
default typing in `ObjectMapper`
(reported by @dvhvsekhar)
#4248: `ThrowableDeserializer` does not handle `null` well for `cause`

2.16.1 (not yet released)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.fasterxml.jackson.databind.deser.std;

import java.io.IOException;
import java.util.Arrays;

import com.fasterxml.jackson.core.*;

Expand Down Expand Up @@ -97,18 +98,22 @@ public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) t
return ctxt.handleMissingInstantiator(handledType(), getValueInstantiator(), p,
"Throwable needs a default constructor, a single-String-arg constructor; or explicit @JsonCreator");
}

Throwable throwable = null;
Object[] pending = null;
Throwable[] suppressed = null;
int pendingIx = 0;

for (; !p.hasToken(JsonToken.END_OBJECT); p.nextToken()) {
String propName = p.currentName();
SettableBeanProperty prop = _beanProperties.find(propName);
p.nextToken(); // to point to field value

if (prop != null) { // normal case
// 07-Dec-2023, tatu: [databind#4248] Interesting that "cause"
// with `null` blows up. So, avoid.
if ("cause".equals(prop.getName())
&& p.hasToken(JsonToken.VALUE_NULL)) {
continue;
}
if (throwable != null) {
prop.deserializeAndSet(p, ctxt, throwable);
continue;
Expand All @@ -117,6 +122,13 @@ public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) t
if (pending == null) {
int len = _beanProperties.size();
pending = new Object[len + len];
} else if (pendingIx == pending.length) {
// NOTE: only occurs with duplicate properties, possible
// with some formats (most notably XML; but possibly with
// JSON if duplicate detection not enabled). Most likely
// only occurs with malicious content so use linear buffer
// resize (no need to optimize performance)
pending = Arrays.copyOf(pending, pendingIx + 16);
}
pending[pendingIx++] = prop;
pending[pendingIx++] = prop.deserialize(p, ctxt);
Expand All @@ -142,7 +154,13 @@ public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) t
continue;
}
if (PROP_NAME_SUPPRESSED.equalsIgnoreCase(propName)) { // or "suppressed"?
suppressed = ctxt.readValue(p, Throwable[].class);
// 07-Dec-2023, tatu: Not sure how/why, but JSON Null is otherwise
// not handled with such call so...
if (p.hasToken(JsonToken.VALUE_NULL)) {
suppressed = null;
} else {
suppressed = ctxt.readValue(p, Throwable[].class);
}
continue;
}
if (PROP_NAME_LOCALIZED_MESSAGE.equalsIgnoreCase(propName)) {
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -693,8 +693,19 @@ public static String getTypeDescription(JavaType fullType)
if (fullType == null) {
return "[null]";
}
// 07-Dec-2023, tatu: Instead of cryptic notation for array types
// (JLS-specified for JDK deserialization), let's use trailing "[]"s
// to indicate dimensions instead
int arrays = 0;
while (fullType.isArrayType()) {
++arrays;
fullType = fullType.getContentType();
}
StringBuilder sb = new StringBuilder(80).append('`');
sb.append(fullType.toCanonical());
while (arrays-- > 0) {
sb.append("[]");
}
return sb.append('`').toString();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ static class MyNoArgException extends Exception

private final ObjectMapper MAPPER = new ObjectMapper();

public void testIOException() throws IOException
public void testIOException() throws Exception
{
IOException ioe = new IOException("TEST");
String json = MAPPER.writerWithDefaultPrettyPrinter()
Expand All @@ -65,7 +65,7 @@ public void testIOException() throws IOException
assertEquals(ioe.getMessage(), result.getMessage());
}

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

public void testWithNullMessage() throws IOException
public void testWithNullMessage() throws Exception
{
final ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
Expand All @@ -92,14 +92,14 @@ public void testWithNullMessage() throws IOException
assertNull(result.getMessage());
}

public void testNoArgsException() throws IOException
public void testNoArgsException() throws Exception
{
MyNoArgException exc = MAPPER.readValue("{}", MyNoArgException.class);
assertNotNull(exc);
}

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

public void testExceptionCauseDeserialization() throws IOException
public void testExceptionCauseDeserialization() throws Exception
{
ObjectMapper mapper = new ObjectMapper();

Expand All @@ -139,7 +139,7 @@ public void testExceptionCauseDeserialization() throws IOException
}


public void testSuppressedGenericThrowableDeserialization() throws IOException
public void testSuppressedGenericThrowableDeserialization() throws Exception
{
ObjectMapper mapper = new ObjectMapper();

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

public void testSuppressedTypedExceptionDeserialization() throws IOException
public void testSuppressedTypedExceptionDeserialization() throws Exception
{
PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator.builder()
.allowIfSubTypeIsArray()
Expand Down Expand Up @@ -231,7 +231,7 @@ public void testSingleValueArrayDeserializationException() throws Exception {
}

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

// [databind#1842]
public void testNullAsMessage() throws IOException
public void testNullAsMessage() throws Exception
{
Exception exc = MAPPER.readValue(a2q(
"{'message':null, 'localizedMessage':null }"
Expand Down Expand Up @@ -278,4 +278,24 @@ private void _testRoundtripWith(ObjectMapper mapper) throws Exception
assertNotNull(result.getCause());
assertEquals(root.getMessage(), result.getCause().getMessage());
}

// [databind#4248]
public void testWithDups() throws Exception
{
// NOTE: by default JSON parser does NOT fail on duplicate properties;
// we only use them to mimic formats like XML where duplicates can occur
// (or, malicious JSON...)
final StringBuilder sb = new StringBuilder(100);
sb.append("{");
sb.append("'suppressed': [],\n");
sb.append("'cause': null,\n");
for (int i = 0; i < 10; ++i) { // just needs to be more than max distinct props
sb.append("'stackTrace': [],\n");
}
sb.append("'message': 'foo',\n");
sb.append("'localizedMessage': 'bar'\n}");
IOException exc = MAPPER.readValue(a2q(sb.toString()), IOException.class);
assertNotNull(exc);
assertEquals("foo", exc.getLocalizedMessage());
}
}