@@ -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 )
0 commit comments