Skip to content

Commit f27df63

Browse files
committed
Fix #3497
1 parent 4bae6cd commit f27df63

File tree

7 files changed

+159
-62
lines changed

7 files changed

+159
-62
lines changed

release-notes/VERSION-2.x

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Project: jackson-databind
2525
(reported by Deniz H)
2626
#3476: Implement `JsonNodeFeature.WRITE_NULL_PROPERTIES` to allow skipping
2727
JSON `null` values on writing
28+
#3497: Deserialization of Throwables with PropertyNamingStrategy does not work
2829

2930
2.13.4 (not yet released)
3031

src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2125,7 +2125,7 @@ public JsonDeserializer<?> findDefaultDeserializer(DeserializationContext ctxt,
21252125
if (deser != null) {
21262126
return deser;
21272127
}
2128-
return JdkDeserializers.find(rawType, clsName);
2128+
return JdkDeserializers.find(ctxt, rawType, clsName);
21292129
}
21302130

21312131
protected JavaType _findRemappedType(DeserializationConfig config, Class<?> rawType) throws JsonMappingException {

src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerFactory.java

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -437,8 +437,14 @@ public JsonDeserializer<Object> buildThrowableDeserializer(DeserializationContex
437437
}
438438
AnnotatedMethod am = beanDesc.findMethod("initCause", INIT_CAUSE_PARAMS);
439439
if (am != null) { // should never be null
440+
// [databind#3497]: must consider possible PropertyNamingStrategy
441+
String name = "cause";
442+
PropertyNamingStrategy pts = config.getPropertyNamingStrategy();
443+
if (pts != null) {
444+
name = pts.nameForSetterMethod(config, am, "cause");
445+
}
440446
SimpleBeanPropertyDefinition propDef = SimpleBeanPropertyDefinition.construct(ctxt.getConfig(), am,
441-
new PropertyName("cause"));
447+
new PropertyName(name));
442448
SettableBeanProperty prop = constructSettableProperty(ctxt, beanDesc, propDef,
443449
am.getParameterType(0));
444450
if (prop != null) {
@@ -447,16 +453,6 @@ public JsonDeserializer<Object> buildThrowableDeserializer(DeserializationContex
447453
builder.addOrReplaceProperty(prop, true);
448454
}
449455
}
450-
451-
// And also need to ignore "localizedMessage"
452-
builder.addIgnorable("localizedMessage");
453-
// Java 7 also added "getSuppressed", skip if we have such data:
454-
builder.addIgnorable("suppressed");
455-
// As well as "message": it will be passed via constructor,
456-
// as there's no 'setMessage()' method
457-
// 23-Jan-2018, tatu: ... although there MAY be Creator Property... which is problematic
458-
// builder.addIgnorable("message");
459-
460456
// update builder now that all information is in?
461457
if (_factoryConfig.hasDeserializerModifiers()) {
462458
for (BeanDeserializerModifier mod : _factoryConfig.deserializerModifiers()) {
@@ -468,7 +464,7 @@ public JsonDeserializer<Object> buildThrowableDeserializer(DeserializationContex
468464
// At this point it ought to be a BeanDeserializer; if not, must assume
469465
// it's some other thing that can handle deserialization ok...
470466
if (deserializer instanceof BeanDeserializer) {
471-
deserializer = new ThrowableDeserializer((BeanDeserializer) deserializer);
467+
deserializer = ThrowableDeserializer.construct(ctxt, (BeanDeserializer) deserializer);
472468
}
473469

474470
// may have modifier(s) that wants to modify or replace serializer we just built:

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,22 @@ public class JdkDeserializers
3030
for (Class<?> cls : FromStringDeserializer.types()) { _classNames.add(cls.getName()); }
3131
}
3232

33+
/**
34+
* @deprecated Since 2.14 use the variant that takes one more argument
35+
*/
36+
@Deprecated // since 2.14
3337
public static JsonDeserializer<?> find(Class<?> rawType, String clsName)
38+
throws JsonMappingException
39+
{
40+
return find(null, rawType, clsName);
41+
}
42+
43+
/**
44+
* @since 2.14
45+
*/
46+
public static JsonDeserializer<?> find(DeserializationContext ctxt,
47+
Class<?> rawType, String clsName)
48+
throws JsonMappingException
3449
{
3550
if (_classNames.contains(clsName)) {
3651
JsonDeserializer<?> d = FromStringDeserializer.findDeserializer(rawType);
@@ -41,7 +56,7 @@ public static JsonDeserializer<?> find(Class<?> rawType, String clsName)
4156
return new UUIDDeserializer();
4257
}
4358
if (rawType == StackTraceElement.class) {
44-
return new StackTraceElementDeserializer();
59+
return StackTraceElementDeserializer.construct(ctxt);
4560
}
4661
if (rawType == AtomicBoolean.class) {
4762
return new AtomicBooleanDeserializer();

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

Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,60 +7,54 @@
77

88
import com.fasterxml.jackson.databind.DeserializationContext;
99
import com.fasterxml.jackson.databind.DeserializationFeature;
10+
import com.fasterxml.jackson.databind.JsonDeserializer;
11+
import com.fasterxml.jackson.databind.JsonMappingException;
1012

1113
public class StackTraceElementDeserializer
1214
extends StdScalarDeserializer<StackTraceElement>
1315
{
1416
private static final long serialVersionUID = 1L;
1517

16-
public StackTraceElementDeserializer() { super(StackTraceElement.class); }
18+
protected final JsonDeserializer<?> _adapterDeserializer;
19+
20+
@Deprecated // since 2.14
21+
public StackTraceElementDeserializer() {
22+
this(null);
23+
}
24+
25+
protected StackTraceElementDeserializer(JsonDeserializer<?> ad)
26+
{
27+
super(StackTraceElement.class);
28+
_adapterDeserializer = ad;
29+
}
30+
31+
/**
32+
* @since 2.14
33+
*/
34+
public static JsonDeserializer<?> construct(DeserializationContext ctxt) throws JsonMappingException {
35+
// 26-May-2022, tatu: for legacy use, need to do this:
36+
if (ctxt == null) {
37+
return new StackTraceElementDeserializer();
38+
}
39+
JsonDeserializer<?> adapterDeser = ctxt.findNonContextualValueDeserializer(ctxt.constructType(Adapter.class));
40+
return new StackTraceElementDeserializer(adapterDeser);
41+
}
1742

1843
@Override
1944
public StackTraceElement deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
2045
{
2146
JsonToken t = p.currentToken();
22-
// Must get an Object
23-
if (t == JsonToken.START_OBJECT) {
24-
String className = "", methodName = "", fileName = "";
25-
// Java 9 adds couple more things
26-
String moduleName = null, moduleVersion = null;
27-
String classLoaderName = null;
28-
int lineNumber = -1;
2947

30-
while ((t = p.nextValue()) != JsonToken.END_OBJECT) {
31-
String propName = p.currentName();
32-
// TODO: with Java 8, convert to switch
33-
if ("className".equals(propName)) {
34-
className = p.getText();
35-
} else if ("classLoaderName".equals(propName)) {
36-
classLoaderName = p.getText();
37-
} else if ("fileName".equals(propName)) {
38-
fileName = p.getText();
39-
} else if ("lineNumber".equals(propName)) {
40-
if (t.isNumeric()) {
41-
lineNumber = p.getIntValue();
42-
} else {
43-
lineNumber = _parseIntPrimitive(p, ctxt);
44-
}
45-
} else if ("methodName".equals(propName)) {
46-
methodName = p.getText();
47-
} else if ("nativeMethod".equals(propName)) {
48-
// no setter, not passed via constructor: ignore
49-
} else if ("moduleName".equals(propName)) {
50-
moduleName = p.getText();
51-
} else if ("moduleVersion".equals(propName)) {
52-
moduleVersion = p.getText();
53-
} else if ("declaringClass".equals(propName)
54-
|| "format".equals(propName)) {
55-
// 01-Nov-2017: [databind#1794] Not sure if we should but... let's prune it for now
56-
;
57-
} else {
58-
handleUnknownProperty(p, ctxt, _valueClass, propName);
59-
}
60-
p.skipChildren(); // just in case we might get structured values
48+
// Must get an Object
49+
if (t == JsonToken.START_OBJECT || t == JsonToken.FIELD_NAME) {
50+
Adapter adapted;
51+
// 26-May-2022, tatu: for legacy use, need to do this:
52+
if (_adapterDeserializer == null) {
53+
adapted = ctxt.readValue(p, Adapter.class);
54+
} else {
55+
adapted = (Adapter) _adapterDeserializer.deserialize(p, ctxt);
6156
}
62-
return constructValue(ctxt, className, methodName, fileName, lineNumber,
63-
moduleName, moduleVersion, classLoaderName);
57+
return constructValue(ctxt, adapted);
6458
} else if (t == JsonToken.START_ARRAY && ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
6559
p.nextToken();
6660
final StackTraceElement value = deserialize(p, ctxt);
@@ -72,6 +66,18 @@ public StackTraceElement deserialize(JsonParser p, DeserializationContext ctxt)
7266
return (StackTraceElement) ctxt.handleUnexpectedToken(_valueClass, p);
7367
}
7468

69+
/**
70+
* @since 2.14
71+
*/
72+
protected StackTraceElement constructValue(DeserializationContext ctxt,
73+
Adapter adapted)
74+
{
75+
return constructValue(ctxt, adapted.className, adapted.methodName,
76+
adapted.fileName, adapted.lineNumber,
77+
adapted.moduleName, adapted.moduleVersion,
78+
adapted.classLoaderName);
79+
}
80+
7581
@Deprecated // since 2.9
7682
protected StackTraceElement constructValue(DeserializationContext ctxt,
7783
String className, String methodName, String fileName, int lineNumber,
@@ -89,8 +95,24 @@ protected StackTraceElement constructValue(DeserializationContext ctxt,
8995
String className, String methodName, String fileName, int lineNumber,
9096
String moduleName, String moduleVersion, String classLoaderName)
9197
{
92-
// 21-May-2016, tatu: With Java 9, need to use different constructor, probably
98+
// 21-May-2016, tatu: With Java 9, could use different constructor, probably
9399
// via different module, and throw exception here if extra args passed
94100
return new StackTraceElement(className, methodName, fileName, lineNumber);
95101
}
102+
103+
/**
104+
* Intermediate class used both for convenience of binding and
105+
* to support {@code PropertyNamingStrategy}.
106+
*
107+
* @since 2.14
108+
*/
109+
public final static class Adapter {
110+
// NOTE: some String fields must not be nulls
111+
public String className = "", classLoaderName;
112+
public String declaringClass, format;
113+
public String fileName = "", methodName = "";
114+
public int lineNumber = -1;
115+
public String moduleName, moduleVersion;
116+
public boolean nativeMethod;
117+
}
96118
}

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

Lines changed: 35 additions & 5 deletions
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.Iterator;
45

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

@@ -14,25 +15,43 @@
1415
* override some aspects like instance construction.
1516
*/
1617
public class ThrowableDeserializer
17-
extends BeanDeserializer
18+
extends BeanDeserializer // not the greatest idea but...
1819
{
1920
private static final long serialVersionUID = 1L;
2021

2122
protected final static String PROP_NAME_MESSAGE = "message";
2223
protected final static String PROP_NAME_SUPPRESSED = "suppressed";
2324

25+
protected final static String PROP_NAME_LOCALIZED_MESSAGE = "localizedMessage";
26+
2427
/*
2528
/**********************************************************************
2629
/* Life-cycle
2730
/**********************************************************************
2831
*/
2932

33+
@Deprecated // since 2.14
3034
public ThrowableDeserializer(BeanDeserializer baseDeserializer) {
3135
super(baseDeserializer);
3236
// need to disable this, since we do post-processing
3337
_vanillaProcessing = false;
3438
}
3539

40+
public static ThrowableDeserializer construct(DeserializationContext ctxt,
41+
BeanDeserializer baseDeserializer)
42+
{
43+
// 27-May-2022, tatu: TODO -- handle actual renaming of fields to support
44+
// strategies like kebab- and snake-case where there are changes beyond
45+
// simple upper-/lower-casing
46+
/*
47+
PropertyNamingStrategy pts = ctxt.getConfig().getPropertyNamingStrategy();
48+
if (pts != null) {
49+
}
50+
*/
51+
return new ThrowableDeserializer(baseDeserializer);
52+
}
53+
54+
3655
/**
3756
* Alternative constructor used when creating "unwrapping" deserializers
3857
*/
@@ -106,25 +125,36 @@ public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) t
106125
}
107126

108127
// Maybe it's "message"?
109-
if (PROP_NAME_MESSAGE.equals(propName)) {
128+
129+
// 26-May-2022, tatu: [databind#3497] To support property naming strategies,
130+
// should ideally mangle property names. But for now let's cheat; works
131+
// for case-changing although not for kebab/snake cases and "localizedMessage"
132+
if (PROP_NAME_MESSAGE.equalsIgnoreCase(propName)) {
110133
if (hasStringCreator) {
111134
throwable = (Throwable) _valueInstantiator.createFromString(ctxt, p.getValueAsString());
112135
continue;
113136
}
114-
} else if (PROP_NAME_SUPPRESSED.equals(propName)) { // or "suppressed"?
115-
suppressed = ctxt.readValue(p, Throwable[].class);
116-
continue;
137+
// fall through
117138
}
118139

119140
// Things marked as ignorable should not be passed to any setter
120141
if ((_ignorableProps != null) && _ignorableProps.contains(propName)) {
121142
p.skipChildren();
122143
continue;
123144
}
145+
if (PROP_NAME_SUPPRESSED.equalsIgnoreCase(propName)) { // or "suppressed"?
146+
suppressed = ctxt.readValue(p, Throwable[].class);
147+
continue;
148+
}
149+
if (PROP_NAME_LOCALIZED_MESSAGE.equalsIgnoreCase(propName)) {
150+
p.skipChildren();
151+
continue;
152+
}
124153
if (_anySetter != null) {
125154
_anySetter.deserializeAndSet(p, ctxt, throwable, propName);
126155
continue;
127156
}
157+
128158
// 23-Jan-2018, tatu: One concern would be `message`, but without any-setter or single-String-ctor
129159
// (or explicit constructor). We could just ignore it but for now, let it fail
130160

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,13 @@ public void testWithCreator() throws IOException
7373
MyException result = MAPPER.readValue(json, MyException.class);
7474
assertEquals(MSG, result.getMessage());
7575
assertEquals(3, result.value);
76-
assertEquals(1, result.stuff.size());
76+
77+
// 27-May-2022, tatu: With [databind#3497] we actually get 3, not 1
78+
// "extra" things exposed
79+
assertEquals(3, result.stuff.size());
7780
assertEquals(result.getFoo(), result.stuff.get("foo"));
81+
assertEquals("the message", result.stuff.get("localizedMessage"));
82+
assertTrue(result.stuff.containsKey("suppressed"));
7883
}
7984

8085
public void testWithNullMessage() throws IOException
@@ -245,4 +250,32 @@ public void testNullAsMessage() throws IOException
245250
assertNull(exc.getMessage());
246251
assertNull(exc.getLocalizedMessage());
247252
}
253+
254+
// [databind#3497]: round-trip with naming strategy
255+
public void testRoundtripWithoutNamingStrategy() throws Exception
256+
{
257+
_testRoundtripWith(MAPPER);
258+
}
259+
260+
public void testRoundtripWithNamingStrategy() throws Exception
261+
{
262+
final ObjectMapper renamingMapper = JsonMapper.builder()
263+
.propertyNamingStrategy(PropertyNamingStrategies.UPPER_CAMEL_CASE)
264+
.build();
265+
_testRoundtripWith(renamingMapper);
266+
}
267+
268+
private void _testRoundtripWith(ObjectMapper mapper) throws Exception
269+
{
270+
Exception root = new Exception("Root cause");
271+
Exception leaf = new Exception("Leaf message", root);
272+
273+
final String json = mapper.writerWithDefaultPrettyPrinter()
274+
.writeValueAsString(leaf);
275+
Exception result = mapper.readValue(json, Exception.class);
276+
277+
assertEquals(leaf.getMessage(), result.getMessage());
278+
assertNotNull(result.getCause());
279+
assertEquals(root.getMessage(), result.getCause().getMessage());
280+
}
248281
}

0 commit comments

Comments
 (0)