Skip to content

Commit f59cb9a

Browse files
authored
fix #4680 : Custom key deserialiser registered for Object.class is ignored on nested JSON (#4684)
1 parent 85f4b55 commit f59cb9a

File tree

3 files changed

+87
-11
lines changed

3 files changed

+87
-11
lines changed

release-notes/VERSION-2.x

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ Project: jackson-databind
2020
#4676: Support other enum naming strategies than camelCase
2121
(requested by @hajdamak)
2222
(contributed by Lars B)
23+
#4680: Custom key deserialiser registered for Object.class is ignored on nested JSON
24+
(reported by @devdanylo)
25+
(fix by Joo-Hyuk K)
2326
#4773: `SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS` should not apply to Maps
2427
with uncomparable keys
2528
(requested by @nathanukey)

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

+83-7
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ public class UntypedObjectDeserializer
4747

4848
protected JsonDeserializer<Object> _numberDeserializer;
4949

50+
/**
51+
* Object.class may also have custom key deserializer
52+
*
53+
* @since 2.19
54+
*/
55+
private KeyDeserializer _customKeyDeserializer;
56+
5057
/**
5158
* If {@link java.util.List} has been mapped to non-default implementation,
5259
* we'll store type here
@@ -73,7 +80,7 @@ public class UntypedObjectDeserializer
7380
*/
7481
@Deprecated
7582
public UntypedObjectDeserializer() {
76-
this(null, null);
83+
this(null, (JavaType) null);
7784
}
7885

7986
public UntypedObjectDeserializer(JavaType listType, JavaType mapType) {
@@ -95,6 +102,7 @@ public UntypedObjectDeserializer(UntypedObjectDeserializer base,
95102
_numberDeserializer = (JsonDeserializer<Object>) numberDeser;
96103
_listType = base._listType;
97104
_mapType = base._mapType;
105+
_customKeyDeserializer = base._customKeyDeserializer;
98106
_nonMerging = base._nonMerging;
99107
}
100108

@@ -111,9 +119,27 @@ protected UntypedObjectDeserializer(UntypedObjectDeserializer base,
111119
_numberDeserializer = base._numberDeserializer;
112120
_listType = base._listType;
113121
_mapType = base._mapType;
122+
_customKeyDeserializer = base._customKeyDeserializer;
114123
_nonMerging = nonMerging;
115124
}
116125

126+
/**
127+
* @since 2.19
128+
*/
129+
protected UntypedObjectDeserializer(UntypedObjectDeserializer base,
130+
KeyDeserializer keyDeser)
131+
{
132+
super(Object.class);
133+
_mapDeserializer = base._mapDeserializer;
134+
_listDeserializer = base._listDeserializer;
135+
_stringDeserializer = base._stringDeserializer;
136+
_numberDeserializer = base._numberDeserializer;
137+
_listType = base._listType;
138+
_mapType = base._mapType;
139+
_nonMerging = base._nonMerging;
140+
_customKeyDeserializer = keyDeser;
141+
}
142+
117143
/*
118144
/**********************************************************
119145
/* Initialization
@@ -190,19 +216,32 @@ public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
190216
// 14-Jun-2017, tatu: [databind#1625]: may want to block merging, for root value
191217
boolean preventMerge = (property == null)
192218
&& Boolean.FALSE.equals(ctxt.getConfig().getDefaultMergeable(Object.class));
219+
// Since 2.19, 31-Aug-2024: [databind#4680] Allow custom key deserializer for Object.class
220+
KeyDeserializer customKeyDeser = ctxt.findKeyDeserializer(ctxt.constructType(Object.class), property);
221+
// but make sure to ignore standard/default key deserializer (perf optimization)
222+
if (customKeyDeser != null) {
223+
if (ClassUtil.isJacksonStdImpl(customKeyDeser)) {
224+
customKeyDeser = null;
225+
}
226+
}
193227
// 20-Apr-2014, tatu: If nothing custom, let's use "vanilla" instance,
194228
// simpler and can avoid some of delegation
195229
if ((_stringDeserializer == null) && (_numberDeserializer == null)
196230
&& (_mapDeserializer == null) && (_listDeserializer == null)
231+
&& (customKeyDeser == null) // [databind#4680] Since 2.19 : Allow custom key deserializer for Object.class
197232
&& getClass() == UntypedObjectDeserializer.class) {
198233
return UntypedObjectDeserializerNR.instance(preventMerge);
199234
}
200235

236+
UntypedObjectDeserializer deser = this;
201237
if (preventMerge != _nonMerging) {
202-
return new UntypedObjectDeserializer(this, preventMerge);
238+
deser = new UntypedObjectDeserializer(deser, preventMerge);
203239
}
204-
205-
return this;
240+
// [databind#4680] Since 2.19 : Allow custom key deserializer for Object.class
241+
if (customKeyDeser != null) {
242+
deser = new UntypedObjectDeserializer(deser, customKeyDeser);
243+
}
244+
return deser;
206245
}
207246

208247
/*
@@ -496,6 +535,7 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOE
496535
// empty map might work; but caller may want to modify... so better just give small modifiable
497536
return new LinkedHashMap<>(2);
498537
}
538+
key1 = _customDeserializeKey(key1, ctxt);
499539
// minor optimization; let's handle 1 and 2 entry cases separately
500540
// 24-Mar-2015, tatu: Ideally, could use one of 'nextXxx()' methods, but for
501541
// that we'd need new method(s) in JsonDeserializer. So not quite yet.
@@ -508,6 +548,8 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOE
508548
result.put(key1, value1);
509549
return result;
510550
}
551+
key2 = _customDeserializeKey(key2, ctxt);
552+
511553
p.nextToken();
512554
Object value2 = deserialize(p, ctxt);
513555

@@ -521,6 +563,8 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOE
521563
}
522564
return result;
523565
}
566+
key = _customDeserializeKey(key, ctxt);
567+
524568
// And then the general case; default map size is 16
525569
LinkedHashMap<String, Object> result = new LinkedHashMap<>();
526570
result.put(key1, value1);
@@ -535,9 +579,9 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOE
535579
final Object oldValue = result.put(key, newValue);
536580
if (oldValue != null) {
537581
return _mapObjectWithDups(p, ctxt, result, key, oldValue, newValue,
538-
p.nextFieldName());
582+
_customDeserializeNullableKey(p.nextFieldName(), ctxt));
539583
}
540-
} while ((key = p.nextFieldName()) != null);
584+
} while ((key = _customDeserializeNullableKey(p.nextFieldName(), ctxt)) != null);
541585
return result;
542586
}
543587

@@ -559,12 +603,44 @@ protected Object _mapObjectWithDups(JsonParser p, DeserializationContext ctxt,
559603
if ((oldValue != null) && squashDups) {
560604
_squashDups(result, key, oldValue, newValue);
561605
}
562-
nextKey = p.nextFieldName();
606+
nextKey = _customDeserializeNullableKey(p.nextFieldName(), ctxt);
563607
}
564608

565609
return result;
566610
}
567611

612+
/**
613+
* Helper function to allow custom key deserialization without null handling.
614+
* Similar to {@link #_customDeserializeNullableKey(String, DeserializationContext)}, but
615+
* null handling is done by the caller.
616+
*
617+
* @returns Custom-deserialized key if both custom key deserializer is set.
618+
* Otherwise the original key.
619+
*/
620+
private final String _customDeserializeKey(String key, DeserializationContext ctxt) throws IOException {
621+
if (_customKeyDeserializer != null) {
622+
return (String) _customKeyDeserializer.deserializeKey(key, ctxt);
623+
}
624+
return key;
625+
}
626+
627+
/**
628+
* Helper function to allow custom key deserialization with null handling.
629+
* Similar to {@link #_customDeserializeKey(String, DeserializationContext)}, but instead
630+
* only returns custom-deserialized key if key is not null.
631+
*
632+
* @returns Custom-deserialized key if both custom key deserializer is set and key is not null.
633+
* Otherwise the original key.
634+
*/
635+
private final String _customDeserializeNullableKey(String key, DeserializationContext ctxt) throws IOException {
636+
if (_customKeyDeserializer != null) {
637+
if (key != null) {
638+
return (String) _customKeyDeserializer.deserializeKey(key, ctxt);
639+
}
640+
}
641+
return key;
642+
}
643+
568644
@SuppressWarnings("unchecked")
569645
private void _squashDups(final Map<String, Object> result, String key,
570646
Object oldValue, Object newValue)

src/test/java/com/fasterxml/jackson/databind/tofix/CustomObjectKeyDeserializer4680Test.java renamed to src/test/java/com/fasterxml/jackson/databind/deser/CustomObjectKeyDeserializer4680Test.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.fasterxml.jackson.databind.tofix;
1+
package com.fasterxml.jackson.databind.deser;
22

33
import java.util.Map;
44

@@ -10,16 +10,13 @@
1010
import com.fasterxml.jackson.databind.ObjectMapper;
1111
import com.fasterxml.jackson.databind.json.JsonMapper;
1212
import com.fasterxml.jackson.databind.module.SimpleModule;
13-
import com.fasterxml.jackson.databind.testutil.failure.JacksonTestFailureExpected;
1413

1514
import static org.junit.jupiter.api.Assertions.assertEquals;
1615

1716
// [databind#4680] Custom key deserializer registered for `Object.class` is ignored on nested JSON
1817
public class CustomObjectKeyDeserializer4680Test
1918
{
20-
2119
@SuppressWarnings("unchecked")
22-
@JacksonTestFailureExpected
2320
@Test
2421
void testCustomKeyDeserializer()
2522
throws Exception

0 commit comments

Comments
 (0)