Skip to content

Commit 3ebd444

Browse files
committed
Fix #3484
1 parent 9743148 commit 3ebd444

File tree

3 files changed

+118
-29
lines changed

3 files changed

+118
-29
lines changed

release-notes/VERSION-2.x

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Project: jackson-databind
4545
#3481: Filter method only got called once if the field is null when using
4646
`@JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = SomeFieldFilter.class)`
4747
(contributed by AmiDavidW@github)
48+
#3484: Update `MapDeserializer` to support `StreamReadCapability.DUPLICATE_PROPERTIES`
4849
#3497: Deserialization of Throwables with PropertyNamingStrategy does not work
4950
#3500: Add optional explicit `JsonSubTypes` repeated names check
5051
(contributed by Igor S)

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

+51-12
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ public class MapDeserializer
101101
*/
102102
protected IgnorePropertiesUtil.Checker _inclusionChecker;
103103

104+
105+
/**
106+
* Flag used to check, whether the {@link com.fasterxml.jackson.core.StreamReadCapability#DUPLICATE_PROPERTIES}
107+
* can be applied, because the Map has declared value type of {@code java.lang.Object}.
108+
*
109+
* @since 2.14
110+
*/
111+
protected boolean _checkDupSquash;
112+
104113
/*
105114
/**********************************************************
106115
/* Life-cycle
@@ -121,6 +130,7 @@ public MapDeserializer(JavaType mapType, ValueInstantiator valueInstantiator,
121130
_propertyBasedCreator = null;
122131
_standardStringKey = _isStdKeyDeser(mapType, keyDeser);
123132
_inclusionChecker = null;
133+
_checkDupSquash = mapType.getContentType().hasRawClass(Object.class);
124134
}
125135

126136
/**
@@ -143,6 +153,7 @@ protected MapDeserializer(MapDeserializer src)
143153
_inclusionChecker = src._inclusionChecker;
144154

145155
_standardStringKey = src._standardStringKey;
156+
_checkDupSquash = src._checkDupSquash;
146157
}
147158

148159
protected MapDeserializer(MapDeserializer src,
@@ -177,6 +188,7 @@ protected MapDeserializer(MapDeserializer src,
177188
_inclusionChecker = IgnorePropertiesUtil.buildCheckerIfNeeded(ignorable, includable);
178189

179190
_standardStringKey = _isStdKeyDeser(_containerType, keyDeser);
191+
_checkDupSquash = src._checkDupSquash;
180192
}
181193

182194
/**
@@ -434,11 +446,9 @@ public Map<Object,Object> deserialize(JsonParser p, DeserializationContext ctxt)
434446
case JsonTokenId.ID_FIELD_NAME:
435447
final Map<Object,Object> result = (Map<Object,Object>) _valueInstantiator.createUsingDefault(ctxt);
436448
if (_standardStringKey) {
437-
_readAndBindStringKeyMap(p, ctxt, result);
438-
return result;
449+
return _readAndBindStringKeyMap(p, ctxt, result);
439450
}
440-
_readAndBind(p, ctxt, result);
441-
return result;
451+
return _readAndBind(p, ctxt, result);
442452
case JsonTokenId.ID_STRING:
443453
// (empty) String may be ok however; or single-String-arg ctor
444454
return _deserializeFromString(p, ctxt);
@@ -499,7 +509,7 @@ public Object deserializeWithType(JsonParser p, DeserializationContext ctxt,
499509
/**********************************************************
500510
*/
501511

502-
protected final void _readAndBind(JsonParser p, DeserializationContext ctxt,
512+
protected final Map<Object,Object> _readAndBind(JsonParser p, DeserializationContext ctxt,
503513
Map<Object,Object> result) throws IOException
504514
{
505515
final KeyDeserializer keyDes = _keyDeserializer;
@@ -520,7 +530,7 @@ protected final void _readAndBind(JsonParser p, DeserializationContext ctxt,
520530
JsonToken t = p.currentToken();
521531
if (t != JsonToken.FIELD_NAME) {
522532
if (t == JsonToken.END_OBJECT) {
523-
return;
533+
return result;
524534
}
525535
ctxt.reportWrongTokenException(this, JsonToken.FIELD_NAME, null);
526536
}
@@ -551,22 +561,26 @@ protected final void _readAndBind(JsonParser p, DeserializationContext ctxt,
551561
if (useObjectId) {
552562
referringAccumulator.put(key, value);
553563
} else {
554-
result.put(key, value);
564+
Object oldValue = result.put(key, value);
565+
if (oldValue != null) {
566+
_squashDups(ctxt, result, key, oldValue, value);
567+
}
555568
}
556569
} catch (UnresolvedForwardReference reference) {
557570
handleUnresolvedReference(ctxt, referringAccumulator, key, reference);
558571
} catch (Exception e) {
559572
wrapAndThrow(ctxt, e, result, keyStr);
560573
}
561574
}
575+
return result;
562576
}
563577

564578
/**
565579
* Optimized method used when keys can be deserialized as plain old
566580
* {@link java.lang.String}s, and there is no custom deserialized
567581
* specified.
568582
*/
569-
protected final void _readAndBindStringKeyMap(JsonParser p, DeserializationContext ctxt,
583+
protected final Map<Object,Object> _readAndBindStringKeyMap(JsonParser p, DeserializationContext ctxt,
570584
Map<Object,Object> result) throws IOException
571585
{
572586
final JsonDeserializer<Object> valueDes = _valueDeserializer;
@@ -583,7 +597,7 @@ protected final void _readAndBindStringKeyMap(JsonParser p, DeserializationConte
583597
} else {
584598
JsonToken t = p.currentToken();
585599
if (t == JsonToken.END_OBJECT) {
586-
return;
600+
return result;
587601
}
588602
if (t != JsonToken.FIELD_NAME) {
589603
ctxt.reportWrongTokenException(this, JsonToken.FIELD_NAME, null);
@@ -613,7 +627,10 @@ protected final void _readAndBindStringKeyMap(JsonParser p, DeserializationConte
613627
if (useObjectId) {
614628
referringAccumulator.put(key, value);
615629
} else {
616-
result.put(key, value);
630+
Object oldValue = result.put(key, value);
631+
if (oldValue != null) {
632+
_squashDups(ctxt, result, key, oldValue, value);
633+
}
617634
}
618635
} catch (UnresolvedForwardReference reference) {
619636
handleUnresolvedReference(ctxt, referringAccumulator, key, reference);
@@ -622,6 +639,8 @@ protected final void _readAndBindStringKeyMap(JsonParser p, DeserializationConte
622639
}
623640
}
624641
// 23-Mar-2015, tatu: TODO: verify we got END_OBJECT?
642+
643+
return result;
625644
}
626645

627646
@SuppressWarnings("unchecked")
@@ -661,8 +680,7 @@ public Map<Object,Object> _deserializeUsingCreator(JsonParser p, Deserialization
661680
} catch (Exception e) {
662681
return wrapAndThrow(ctxt, e, _containerType.getRawClass(), key);
663682
}
664-
_readAndBind(p, ctxt, result);
665-
return result;
683+
return _readAndBind(p, ctxt, result);
666684
}
667685
continue;
668686
}
@@ -836,6 +854,27 @@ protected final void _readAndUpdateStringKeyMap(JsonParser p, DeserializationCon
836854
}
837855
}
838856

857+
/**
858+
* @since 2.14
859+
*/
860+
@SuppressWarnings("unchecked")
861+
protected void _squashDups(final DeserializationContext ctxt,
862+
final Map<Object, Object> result,
863+
final Object key, final Object oldValue, final Object newValue)
864+
{
865+
if (_checkDupSquash && ctxt.isEnabled(StreamReadCapability.DUPLICATE_PROPERTIES)) {
866+
if (oldValue instanceof List<?>) {
867+
((List<Object>) oldValue).add(newValue);
868+
result.put(key, oldValue);
869+
} else {
870+
ArrayList<Object> l = new ArrayList<>();
871+
l.add(oldValue);
872+
l.add(newValue);
873+
result.put(key, l);
874+
}
875+
}
876+
}
877+
839878
/*
840879
/**********************************************************
841880
/* Internal methods, other

src/test/java/com/fasterxml/jackson/databind/interop/UntypedObjectWithDupsTest.java

+66-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.fasterxml.jackson.databind.interop;
22

3-
import java.util.Map;
3+
import java.util.*;
44

55
import com.fasterxml.jackson.core.JsonParser;
66
import com.fasterxml.jackson.core.StreamReadCapability;
@@ -13,42 +13,91 @@ public class UntypedObjectWithDupsTest extends BaseMapTest
1313
{
1414
private final ObjectMapper JSON_MAPPER = newJsonMapper();
1515

16+
@SuppressWarnings("serial")
17+
static class StringStringMap extends LinkedHashMap<String,String> { };
18+
19+
private final String DOC_WITH_DUPS = a2q(
20+
"{'hello': 'world',\n"
21+
+ "'lists' : 1,\n"
22+
+ "'lists' : 2,\n"
23+
+ "'lists' : {\n"
24+
+ " 'inner' : 'internal',\n"
25+
+ " 'time' : 123\n"
26+
+ "},\n"
27+
+ "'lists' : 3,\n"
28+
+ "'single' : 'one'\n"
29+
+ "}");
30+
31+
// Testing the baseline non-merging behavior
32+
public void testDocWithDupsNoMerging() throws Exception
33+
{
34+
_verifyDupsNoMerging(Object.class);
35+
_verifyDupsNoMerging(Map.class);
36+
}
37+
1638
// For [dataformat-xml#???]
1739
public void testDocWithDupsAsUntyped() throws Exception
1840
{
19-
_readWriteDocAs(Object.class);
41+
_verifyDupsAreMerged(Object.class);
2042
}
2143

2244
// For [dataformat-xml#498] / [databind#3484]
2345
public void testDocWithDupsAsMap() throws Exception
2446
{
25-
// _readWriteDocAs(Map.class);
47+
_verifyDupsAreMerged(Map.class);
2648
}
2749

28-
private <T> void _readWriteDocAs(Class<T> cls) throws Exception
50+
// And also verify that Maps with values other than `Object` will
51+
// NOT try merging no matter what
52+
public void testDocWithDupsAsNonUntypedMap() throws Exception
53+
{
54+
final String DOC = a2q("{'key':'a','key':'b'}");
55+
assertEquals(a2q("{'key':'b'}"),
56+
_readWriteDupDoc(DOC, StringStringMap.class));
57+
}
58+
59+
/*
60+
///////////////////////////////////////////////////////////////////////
61+
// Helper methods
62+
///////////////////////////////////////////////////////////////////////
63+
*/
64+
65+
/* Method that will verify default JSON behavior of overwriting value
66+
* (no merging).
67+
*/
68+
private <T> void _verifyDupsNoMerging(Class<T> cls) throws Exception
2969
{
30-
final String doc = a2q(
31-
"{'hello': 'world',\n"
32-
+ "'lists' : 1,\n"
33-
+ "'lists' : 2,\n"
34-
+ "'lists' : {\n"
35-
+ " 'inner' : 'internal',\n"
36-
+ " 'time' : 123\n"
37-
+ "},\n"
38-
+ "'lists' : 3,\n"
39-
+ "'single' : 'one'\n"
40-
+ "}");
4170
// This is where need some trickery
4271
T value;
43-
try (JsonParser p = new WithDupsParser(JSON_MAPPER.createParser(doc))) {
72+
try (JsonParser p = JSON_MAPPER.createParser(DOC_WITH_DUPS)) {
4473
value = JSON_MAPPER.readValue(p, cls);
4574
}
4675

4776
String json = JSON_MAPPER.writeValueAsString(value);
4877
assertEquals(a2q(
78+
"{'hello':'world','lists':3,'single':'one'}"),
79+
json);
80+
}
81+
82+
/* Method that will verify alternate behavior (used by XML module f.ex)
83+
* in which duplicate "properties" are merged into `List`s as necessary
84+
*/
85+
private void _verifyDupsAreMerged(Class<?> cls) throws Exception
86+
{
87+
assertEquals(a2q(
4988
"{'hello':'world','lists':[1,2,"
5089
+"{'inner':'internal','time':123},3],'single':'one'}"),
51-
json);
90+
_readWriteDupDoc(DOC_WITH_DUPS, cls));
91+
}
92+
93+
private String _readWriteDupDoc(String doc, Class<?> cls) throws Exception
94+
{
95+
// This is where need some trickery
96+
Object value;
97+
try (JsonParser p = new WithDupsParser(JSON_MAPPER.createParser(doc))) {
98+
value = JSON_MAPPER.readValue(p, cls);
99+
}
100+
return JSON_MAPPER.writeValueAsString(value);
52101
}
53102

54103
/**

0 commit comments

Comments
 (0)