diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 8f1fabbf30..6f7a2b17b0 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -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) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/ThrowableDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/ThrowableDeserializer.java index eafb470f35..dabb6d4e6f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/ThrowableDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/ThrowableDeserializer.java @@ -1,6 +1,7 @@ package com.fasterxml.jackson.databind.deser.std; import java.io.IOException; +import java.util.Arrays; import com.fasterxml.jackson.core.*; @@ -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; @@ -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); @@ -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)) { diff --git a/src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java b/src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java index 40549c9f66..319882261e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/ClassUtil.java @@ -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(); } diff --git a/src/test/java/com/fasterxml/jackson/databind/exc/ExceptionDeserializationTest.java b/src/test/java/com/fasterxml/jackson/databind/exc/ExceptionDeserializationTest.java index b575b49024..6f1161662d 100644 --- a/src/test/java/com/fasterxml/jackson/databind/exc/ExceptionDeserializationTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/exc/ExceptionDeserializationTest.java @@ -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() @@ -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)); @@ -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); @@ -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); @@ -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(); @@ -139,7 +139,7 @@ public void testExceptionCauseDeserialization() throws IOException } - public void testSuppressedGenericThrowableDeserialization() throws IOException + public void testSuppressedGenericThrowableDeserialization() throws Exception { ObjectMapper mapper = new ObjectMapper(); @@ -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() @@ -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': " @@ -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 }" @@ -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()); + } }