Skip to content

Commit eeda3ff

Browse files
committed
Complete "deep merge" support for Maps; next, JsonNode
1 parent b14a79b commit eeda3ff

File tree

3 files changed

+185
-33
lines changed

3 files changed

+185
-33
lines changed

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

+8-7
Original file line numberDiff line numberDiff line change
@@ -731,17 +731,18 @@ protected final void _readAndUpdateStringKeyMap(JsonParser p, DeserializationCon
731731
result.put(key, _nullProvider.getNullValue(ctxt));
732732
continue;
733733
}
734-
Object value = result.get(key);
735-
if (value != null) {
736-
valueDes.deserialize(p, ctxt, value);
737-
continue;
738-
}
739-
if (typeDeser == null) {
734+
Object old = result.get(key);
735+
Object value;
736+
if (old != null) {
737+
value = valueDes.deserialize(p, ctxt, old);
738+
} else if (typeDeser == null) {
740739
value = valueDes.deserialize(p, ctxt);
741740
} else {
742741
value = valueDes.deserializeWithType(p, ctxt, typeDeser);
743742
}
744-
result.put(key, value);
743+
if (value != old) {
744+
result.put(key, value);
745+
}
745746
} catch (Exception e) {
746747
wrapAndThrow(e, result, key);
747748
}

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

+136-23
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,74 @@ public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, Typ
324324
return ctxt.handleUnexpectedToken(Object.class, p);
325325
}
326326

327+
@SuppressWarnings("unchecked")
328+
@Override // since 2.9 (to support deep merge)
329+
public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue)
330+
throws IOException
331+
{
332+
switch (p.getCurrentTokenId()) {
333+
case JsonTokenId.ID_START_OBJECT:
334+
case JsonTokenId.ID_FIELD_NAME:
335+
// 28-Oct-2015, tatu: [databind#989] We may also be given END_OBJECT (similar to FIELD_NAME),
336+
// if caller has advanced to the first token of Object, but for empty Object
337+
case JsonTokenId.ID_END_OBJECT:
338+
if (_mapDeserializer != null) {
339+
return _mapDeserializer.deserialize(p, ctxt, intoValue);
340+
}
341+
if (intoValue instanceof Map<?,?>) {
342+
return mapObject(p, ctxt, (Map<Object,Object>) intoValue);
343+
}
344+
return mapObject(p, ctxt);
345+
case JsonTokenId.ID_START_ARRAY:
346+
if (_listDeserializer != null) {
347+
return _listDeserializer.deserialize(p, ctxt, intoValue);
348+
}
349+
if (intoValue instanceof Collection<?>) {
350+
return mapArray(p, ctxt, (Collection<Object>) intoValue);
351+
}
352+
if (ctxt.isEnabled(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY)) {
353+
return mapArrayToArray(p, ctxt);
354+
}
355+
return mapArray(p, ctxt);
356+
case JsonTokenId.ID_EMBEDDED_OBJECT:
357+
return p.getEmbeddedObject();
358+
case JsonTokenId.ID_STRING:
359+
if (_stringDeserializer != null) {
360+
return _stringDeserializer.deserialize(p, ctxt, intoValue);
361+
}
362+
return p.getText();
363+
364+
case JsonTokenId.ID_NUMBER_INT:
365+
if (_numberDeserializer != null) {
366+
return _numberDeserializer.deserialize(p, ctxt, intoValue);
367+
}
368+
if (ctxt.hasSomeOfFeatures(F_MASK_INT_COERCIONS)) {
369+
return _coerceIntegral(p, ctxt);
370+
}
371+
return p.getNumberValue();
372+
373+
case JsonTokenId.ID_NUMBER_FLOAT:
374+
if (_numberDeserializer != null) {
375+
return _numberDeserializer.deserialize(p, ctxt, intoValue);
376+
}
377+
if (ctxt.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)) {
378+
return p.getDecimalValue();
379+
}
380+
return p.getNumberValue();
381+
case JsonTokenId.ID_TRUE:
382+
return Boolean.TRUE;
383+
case JsonTokenId.ID_FALSE:
384+
return Boolean.FALSE;
385+
386+
case JsonTokenId.ID_NULL:
387+
// 21-Apr-2017, tatu: May need to consider "skip nulls" at some point but...
388+
return null;
389+
default:
390+
}
391+
// easiest to just delegate to "dumb" version for the rest?
392+
return deserialize(p, ctxt);
393+
}
394+
327395
/*
328396
/**********************************************************
329397
/* Internal methods
@@ -373,6 +441,17 @@ protected Object mapArray(JsonParser p, DeserializationContext ctxt) throws IOEx
373441
return result;
374442
}
375443

444+
protected Object mapArray(JsonParser p, DeserializationContext ctxt,
445+
Collection<Object> result) throws IOException
446+
{
447+
// we start by pointing to START_ARRAY. Also, no real merging; array/Collection
448+
// just appends always
449+
while (p.nextToken() != JsonToken.END_ARRAY) {
450+
result.add(deserialize(p, ctxt));
451+
}
452+
return result;
453+
}
454+
376455
/**
377456
* Method called to map a JSON Object into a Java value.
378457
*/
@@ -455,6 +534,36 @@ protected Object[] mapArrayToArray(JsonParser p, DeserializationContext ctxt) th
455534
return buffer.completeAndClearBuffer(values, ptr);
456535
}
457536

537+
protected Object mapObject(JsonParser p, DeserializationContext ctxt,
538+
Map<Object,Object> m) throws IOException
539+
{
540+
JsonToken t = p.getCurrentToken();
541+
if (t == JsonToken.START_OBJECT) {
542+
t = p.nextToken();
543+
}
544+
if (t == JsonToken.END_OBJECT) {
545+
return m;
546+
}
547+
// NOTE: we are guaranteed to point to FIELD_NAME
548+
String key = p.getCurrentName();
549+
do {
550+
p.nextToken();
551+
// and possibly recursive merge here
552+
Object old = m.get(key);
553+
Object newV;
554+
555+
if (old != null) {
556+
newV = deserialize(p, ctxt, old);
557+
} else {
558+
newV = deserialize(p, ctxt);
559+
}
560+
if (newV != old) {
561+
m.put(key, newV);
562+
}
563+
} while ((key = p.nextFieldName()) != null);
564+
return m;
565+
}
566+
458567
/*
459568
/**********************************************************
460569
/* Separate "vanilla" implementation for common case of
@@ -585,6 +694,9 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt, Object into
585694
throws IOException
586695
{
587696
switch (p.getCurrentTokenId()) {
697+
case JsonTokenId.ID_END_OBJECT:
698+
case JsonTokenId.ID_END_ARRAY:
699+
return intoValue;
588700
case JsonTokenId.ID_START_OBJECT:
589701
{
590702
JsonToken t = p.nextToken(); // to get to FIELD_NAME or END_OBJECT
@@ -594,7 +706,24 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt, Object into
594706
}
595707
case JsonTokenId.ID_FIELD_NAME:
596708
if (intoValue instanceof Map<?,?>) {
597-
return mapObject(p, ctxt, (Map<Object,Object>) intoValue);
709+
Map<Object,Object> m = (Map<Object,Object>) intoValue;
710+
// NOTE: we are guaranteed to point to FIELD_NAME
711+
String key = p.getCurrentName();
712+
do {
713+
p.nextToken();
714+
// and possibly recursive merge here
715+
Object old = m.get(key);
716+
Object newV;
717+
if (old != null) {
718+
newV = deserialize(p, ctxt, old);
719+
} else {
720+
newV = deserialize(p, ctxt);
721+
}
722+
if (newV != old) {
723+
m.put(key, newV);
724+
}
725+
} while ((key = p.nextFieldName()) != null);
726+
return intoValue;
598727
}
599728
break;
600729
case JsonTokenId.ID_START_ARRAY:
@@ -606,7 +735,12 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt, Object into
606735
}
607736

608737
if (intoValue instanceof Collection<?>) {
609-
return mapArray(p, ctxt, (Collection<Object>) intoValue);
738+
Collection<Object> c = (Collection<Object>) intoValue;
739+
// NOTE: merge for arrays/Collections means append, can't merge contents
740+
do {
741+
c.add(deserialize(p, ctxt));
742+
} while (p.nextToken() != JsonToken.END_ARRAY);
743+
return intoValue;
610744
}
611745
// 21-Apr-2017, tatu: Should we try to support merging of Object[] values too?
612746
// ... maybe future improvement
@@ -669,15 +803,6 @@ protected Object[] mapArrayToArray(JsonParser p, DeserializationContext ctxt) th
669803
} while (p.nextToken() != JsonToken.END_ARRAY);
670804
return buffer.completeAndClearBuffer(values, ptr);
671805
}
672-
673-
protected Object mapArray(JsonParser p, DeserializationContext ctxt,
674-
Collection<Object> result) throws IOException
675-
{
676-
do {
677-
result.add(deserialize(p, ctxt));
678-
} while (p.nextToken() != JsonToken.END_ARRAY);
679-
return result;
680-
}
681806

682807
/**
683808
* Method called to map a JSON Object into a Java value.
@@ -715,17 +840,5 @@ protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOE
715840
} while ((key = p.nextFieldName()) != null);
716841
return result;
717842
}
718-
719-
protected Object mapObject(JsonParser p, DeserializationContext ctxt,
720-
Map<Object,Object> result) throws IOException
721-
{
722-
// NOTE: we are guaranteed to point to FIELD_NAME
723-
String key = p.getCurrentName();
724-
do {
725-
p.nextToken();
726-
result.put(key, deserialize(p, ctxt));
727-
} while ((key = p.nextFieldName()) != null);
728-
return result;
729-
}
730843
}
731844
}

src/test/java/com/fasterxml/jackson/databind/deser/merge/MapMergeTest.java

+41-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.fasterxml.jackson.databind.deser.merge;
22

3+
import java.util.ArrayList;
34
import java.util.LinkedHashMap;
5+
import java.util.List;
46
import java.util.Map;
57

68
import com.fasterxml.jackson.annotation.JsonMerge;
@@ -49,25 +51,61 @@ public void testShallowMapMerging() throws Exception
4951
}
5052

5153
@SuppressWarnings("unchecked")
52-
public void testDeepMapMerging() throws Exception
54+
public void testDeeperMapMerging() throws Exception
5355
{
5456
// first, create base Map
5557
MergedMap base = new MergedMap("name", "foobar");
5658
Map<String,Object> props = new LinkedHashMap<>();
5759
props.put("default", "yes");
5860
props.put("x", "abc");
61+
Map<String,Object> innerProps = new LinkedHashMap<>();
62+
innerProps.put("z", Integer.valueOf(13));
63+
props.put("extra", innerProps);
5964
base.values.put("props", props);
6065

6166
// to be update
6267
MergedMap v = MAPPER.readerForUpdating(base)
63-
.readValue(aposToQuotes("{'values':{'props':{'x':'xyz','y' : '...'}}}"));
68+
.readValue(aposToQuotes("{'values':{'props':{'x':'xyz','y' : '...','extra':{ 'ab' : true}}}}"));
6469
assertEquals(2, v.values.size());
6570
assertEquals("foobar", v.values.get("name"));
6671
assertNotNull(v.values.get("props"));
6772
props = (Map<String,Object>) v.values.get("props");
68-
assertEquals(3, props.size());
73+
assertEquals(4, props.size());
6974
assertEquals("yes", props.get("default"));
7075
assertEquals("xyz", props.get("x"));
7176
assertEquals("...", props.get("y"));
77+
assertNotNull(props.get("extra"));
78+
innerProps = (Map<String,Object>) props.get("extra");
79+
assertEquals(2, innerProps.size());
80+
assertEquals(Integer.valueOf(13), innerProps.get("z"));
81+
assertEquals(Boolean.TRUE, innerProps.get("ab"));
82+
}
83+
84+
@SuppressWarnings("unchecked")
85+
public void testMapMergingWithArray() throws Exception
86+
{
87+
// first, create base Map
88+
MergedMap base = new MergedMap("name", "foobar");
89+
Map<String,Object> props = new LinkedHashMap<>();
90+
List<String> names = new ArrayList<>();
91+
names.add("foo");
92+
props.put("names", names);
93+
base.values.put("props", props);
94+
props.put("extra", "misc");
95+
96+
// to be update
97+
MergedMap v = MAPPER.readerForUpdating(base)
98+
.readValue(aposToQuotes("{'values':{'props':{'names': [ 'bar' ] }}}"));
99+
assertEquals(2, v.values.size());
100+
assertEquals("foobar", v.values.get("name"));
101+
assertNotNull(v.values.get("props"));
102+
props = (Map<String,Object>) v.values.get("props");
103+
assertEquals(2, props.size());
104+
assertEquals("misc", props.get("extra"));
105+
assertNotNull(props.get("names"));
106+
names = (List<String>) props.get("names");
107+
assertEquals(2, names.size());
108+
assertEquals("foo", names.get(0));
109+
assertEquals("bar", names.get(1));
72110
}
73111
}

0 commit comments

Comments
 (0)