Skip to content
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

fix #4680 : Custom key deserialiser registered for Object.class is ignored on nested JSON #4684

Open
wants to merge 26 commits into
base: 2.19
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f3d38b0
...
JooHyukKim Sep 2, 2024
e832d13
Improve test doc
JooHyukKim Sep 2, 2024
06f8b83
Merge branch '2.18' into fix4680
cowtowncoder Sep 5, 2024
a1fe9cc
Add check if is default key-deserializer
JooHyukKim Sep 5, 2024
9222ade
Improve validation to check if custom-key-deserializer is not null
JooHyukKim Sep 5, 2024
6e04144
Merge branch '2.18' into fix4680
cowtowncoder Sep 12, 2024
149180c
Merge branch '2.18' into fix4680
cowtowncoder Sep 20, 2024
03b365a
Minor renaming, tweaks
cowtowncoder Sep 20, 2024
f91f0a3
Merge branch '2.18' into fix4680
cowtowncoder Sep 20, 2024
8976fef
Remove dup test class
cowtowncoder Sep 20, 2024
e80b337
Minor streamlining
cowtowncoder Sep 20, 2024
1f7dfbc
Add and cover at least 5 keys cases
JooHyukKim Sep 21, 2024
874dd82
Update doc
JooHyukKim Sep 21, 2024
39aa9a1
Merge branch '2.18' into fix4680
cowtowncoder Sep 29, 2024
672c69e
Merge remote-tracking branch 'upstream/2.19' into fix4680
JooHyukKim Sep 29, 2024
2792f28
Add version note
JooHyukKim Sep 29, 2024
83af378
Add "since" to where needed
JooHyukKim Sep 29, 2024
c09b191
Merge branch '2.19' into fix4680
cowtowncoder Oct 2, 2024
517edba
tiny tweaks
cowtowncoder Oct 2, 2024
ef0e18c
some last (?) tweaks
cowtowncoder Oct 2, 2024
1a0cdbf
Add some JavaDoc
JooHyukKim Oct 2, 2024
212bc64
Add comments
JooHyukKim Oct 2, 2024
3c35bcd
Merge branch '2.19' into fix4680
cowtowncoder Oct 3, 2024
f58cdd6
Merge branch 'fix4680' of github.com:JooHyukKim/jackson-databind into…
cowtowncoder Oct 3, 2024
d48f5f6
Merge branch '2.19' into fix4680
cowtowncoder Oct 3, 2024
2989044
Merge branch '2.19' into fix4680
cowtowncoder Oct 3, 2024
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
5 changes: 4 additions & 1 deletion release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ Project: jackson-databind
(contributed by Konstantin M)
#4676: Support other enum naming strategies than camelCase
(requested by @hajdamak)
(contributed by Lars
(contributed by Lars B)
#4680: Custom key deserialiser registered for Object.class is ignored on nested JSON
(reported by @devdanylo)
(fix by Joo-Hyuk K)

2.18.1 (WIP-2024)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ public class UntypedObjectDeserializer

protected JsonDeserializer<Object> _numberDeserializer;

/**
* Object.class may also have custom key deserializer
*
* @since 2.19
*/
private KeyDeserializer _customKeyDeserializer;

/**
* If {@link java.util.List} has been mapped to non-default implementation,
* we'll store type here
Expand All @@ -74,7 +81,7 @@ public class UntypedObjectDeserializer
*/
@Deprecated
public UntypedObjectDeserializer() {
this(null, null);
this(null, (JavaType) null);
}

public UntypedObjectDeserializer(JavaType listType, JavaType mapType) {
Expand All @@ -96,6 +103,7 @@ public UntypedObjectDeserializer(UntypedObjectDeserializer base,
_numberDeserializer = (JsonDeserializer<Object>) numberDeser;
_listType = base._listType;
_mapType = base._mapType;
_customKeyDeserializer = base._customKeyDeserializer;
_nonMerging = base._nonMerging;
}

Expand All @@ -112,9 +120,27 @@ protected UntypedObjectDeserializer(UntypedObjectDeserializer base,
_numberDeserializer = base._numberDeserializer;
_listType = base._listType;
_mapType = base._mapType;
_customKeyDeserializer = base._customKeyDeserializer;
_nonMerging = nonMerging;
}

/**
* @since 2.19
*/
protected UntypedObjectDeserializer(UntypedObjectDeserializer base,
KeyDeserializer keyDeser)
{
super(Object.class);
_mapDeserializer = base._mapDeserializer;
_listDeserializer = base._listDeserializer;
_stringDeserializer = base._stringDeserializer;
_numberDeserializer = base._numberDeserializer;
_listType = base._listType;
_mapType = base._mapType;
_nonMerging = base._nonMerging;
_customKeyDeserializer = keyDeser;
}

/*
/**********************************************************
/* Initialization
Expand Down Expand Up @@ -191,19 +217,32 @@ public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
// 14-Jun-2017, tatu: [databind#1625]: may want to block merging, for root value
boolean preventMerge = (property == null)
&& Boolean.FALSE.equals(ctxt.getConfig().getDefaultMergeable(Object.class));
// Since 2.19, 31-Aug-2024: [databind#4680] Allow custom key deserializer for Object.class
KeyDeserializer customKeyDeser = ctxt.findKeyDeserializer(ctxt.constructType(Object.class), property);
// but make sure to ignore standard/default key deserializer (perf optimization)
if (customKeyDeser != null) {
if (ClassUtil.isJacksonStdImpl(customKeyDeser)) {
customKeyDeser = null;
}
}
// 20-Apr-2014, tatu: If nothing custom, let's use "vanilla" instance,
// simpler and can avoid some of delegation
if ((_stringDeserializer == null) && (_numberDeserializer == null)
&& (_mapDeserializer == null) && (_listDeserializer == null)
&& (customKeyDeser == null) // [databind#4680] Since 2.19 : Allow custom key deserializer for Object.class
&& getClass() == UntypedObjectDeserializer.class) {
return UntypedObjectDeserializerNR.instance(preventMerge);
}

UntypedObjectDeserializer deser = this;
if (preventMerge != _nonMerging) {
return new UntypedObjectDeserializer(this, preventMerge);
deser = new UntypedObjectDeserializer(deser, preventMerge);
}

return this;
// [databind#4680] Since 2.19 : Allow custom key deserializer for Object.class
if (customKeyDeser != null) {
deser = new UntypedObjectDeserializer(deser, customKeyDeser);
}
return deser;
}

/*
Expand Down Expand Up @@ -497,6 +536,7 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOE
// empty map might work; but caller may want to modify... so better just give small modifiable
return new LinkedHashMap<>(2);
}
key1 = _customDeserializeKey(key1, ctxt);
// minor optimization; let's handle 1 and 2 entry cases separately
// 24-Mar-2015, tatu: Ideally, could use one of 'nextXxx()' methods, but for
// that we'd need new method(s) in JsonDeserializer. So not quite yet.
Expand All @@ -509,6 +549,8 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOE
result.put(key1, value1);
return result;
}
key2 = _customDeserializeKey(key2, ctxt);

p.nextToken();
Object value2 = deserialize(p, ctxt);

Expand All @@ -522,6 +564,8 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOE
}
return result;
}
key = _customDeserializeKey(key, ctxt);

// And then the general case; default map size is 16
LinkedHashMap<String, Object> result = new LinkedHashMap<>();
result.put(key1, value1);
Expand All @@ -536,9 +580,9 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOE
final Object oldValue = result.put(key, newValue);
if (oldValue != null) {
return _mapObjectWithDups(p, ctxt, result, key, oldValue, newValue,
p.nextFieldName());
_customDeserializeNullableKey(p.nextFieldName(), ctxt));
}
} while ((key = p.nextFieldName()) != null);
} while ((key = _customDeserializeNullableKey(p.nextFieldName(), ctxt)) != null);
return result;
}

Expand All @@ -560,12 +604,44 @@ protected Object _mapObjectWithDups(JsonParser p, DeserializationContext ctxt,
if ((oldValue != null) && squashDups) {
_squashDups(result, key, oldValue, newValue);
}
nextKey = p.nextFieldName();
nextKey = _customDeserializeNullableKey(p.nextFieldName(), ctxt);
}

return result;
}

/**
* Helper function to allow custom key deserialization without null handling.
* Similar to {@link #_customDeserializeNullableKey(String, DeserializationContext), but
* null handling is done by the caller.
*
* @returns Custom-deserialized key if both custom key deserializer is set.
* Otherwise the original key.
*/
private String _customDeserializeKey(String key, DeserializationContext ctxt) throws IOException {
if (_customKeyDeserializer != null) {
return (String) _customKeyDeserializer.deserializeKey(key, ctxt);
}
return key;
}

/**
* Helper function to allow custom key deserialization with null handling.
* Similar to {@link #_customDeserializeKey(String, DeserializationContext), but instead
* only returns custom-deserialized key if key is not null.
*
* @returns Custom-deserialized key if both custom key deserializer is set and key is not null.
* Otherwise the original key.
*/
private String _customDeserializeNullableKey(String key, DeserializationContext ctxt) throws IOException {
if (_customKeyDeserializer != null) {
if (key != null) {
return (String) _customKeyDeserializer.deserializeKey(key, ctxt);
}
}
return key;
}

@SuppressWarnings("unchecked")
private void _squashDups(final Map<String, Object> result, String key,
Object oldValue, Object newValue)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.fasterxml.jackson.databind.deser;

import java.util.Map;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

// [databind#4680] Custom key deserialiser registered for `Object.class` is ignored on nested JSON
public class CustomKeyDeserializer4680Test {

// Reported test case
@Test
void customKeyDeserializerShouldBeUsedWhenTypeNotDefined() throws Exception {
// GIVEN
String JSON = "{\n" +
" \"name*\": \"Erik\",\n" +
" \"address*\": {\n" +
" \"city*\": {\n" +
" \"id*\": 1,\n" +
" \"name*\": \"Berlin\"\n" +
" },\n" +
" \"street*\": \"Elvirastr\"\n" +
" }\n" +
" }";

ObjectMapper mapper = JsonMapper.builder().addModule(_customKeyDeserModule()).build();

// WHEN
Map<String, Object> result = mapper.readValue(JSON, new TypeReference<Map<String, Object>>() {});
Map<String, Object> addressMap = (Map<String, Object>) result.get("address_");
Map<String, Object> cityMap = (Map<String, Object>) addressMap.get("city_");

// THEN
assertEquals("Erik", result.get("name_"));
assertEquals("Elvirastr", addressMap.get("street_"));
assertEquals(1, cityMap.get("id_"));
assertEquals("Berlin", cityMap.get("name_"));
}

@Test
void customKeyDeserFirst5KeysAsWell() throws Exception {
// Given
// test should check that first 5 keys per level get custom deserialized as well
String JSON = "{\n" +
" \"name1*\": \"Erik1\"," +
" \"name2*\": \"Erik2\"," +
" \"name3*\": \"Erik3\"," +
" \"name4*\": \"Erik4\"," +
" \"name5*\": \"Erik5\"," +
" \"inner*\": {" +
" \"key1*\": \"value1\"," +
" \"key2*\": \"value2\"," +
" \"key3*\": \"value3\"," +
" \"key4*\": \"value4\"," +
" \"key5*\": \"value5\"" +
" }" +
" }";

ObjectMapper mapper = JsonMapper.builder().addModule(_customKeyDeserModule()).build();

// When
Map<String, Object> outerMap = mapper.readValue(JSON, new TypeReference<Map<String, Object>>() {});
Map<String, Object> innerMap = (Map<String, Object>) outerMap.get("inner_");
// Then
// depth 1 works as expected
assertEquals(6, outerMap.keySet().size());
assertEquals(5, innerMap.keySet().size());

// depth 2 works as expected
assertTrue(outerMap.keySet().stream().allMatch(key -> key.endsWith("_")));
assertTrue(innerMap.keySet().stream().allMatch(key -> key.endsWith("_")));
}

private Module _customKeyDeserModule() {
return new SimpleModule("key-sanitization")
.addKeyDeserializer(String.class, new KeyDeserializer() {
@Override
public String deserializeKey(String key, DeserializationContext ctxt) {
return key.replace("*", "_");
}
})
.addKeyDeserializer(Object.class, new KeyDeserializer() {
@Override
public Object deserializeKey(String key, DeserializationContext ctxt) {
return key.replace("*", "_");
}
});
}

}

This file was deleted.