Skip to content

Commit 9cd0d26

Browse files
authored
Allow @JsonMerge with Custom List (#4784)
1 parent 44c01a9 commit 9cd0d26

File tree

3 files changed

+155
-10
lines changed

3 files changed

+155
-10
lines changed

release-notes/VERSION-2.x

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ Project: jackson-databind
1616
#4790: Fix `@JsonAnySetter` issue with "setter" method (related to #4639)
1717
(reported by @bsa01)
1818
(fix by Joo-Hyuk K)
19+
#4783 Possibly wrong behavior of @JsonMerge
20+
(reported by @nlisker)
21+
(fix by Joo-Hyuk K)
1922

2023
2.18.1 (28-Oct-2024)
2124

src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java

+4-10
Original file line numberDiff line numberDiff line change
@@ -824,13 +824,7 @@ public JsonDeserializer<?> createCollectionDeserializer(DeserializationContext c
824824
if (deser == null) {
825825
if (type.isInterface() || type.isAbstract()) {
826826
CollectionType implType = _mapAbstractCollectionType(type, config);
827-
if (implType == null) {
828-
// [databind#292]: Actually, may be fine, but only if polymorphich deser enabled
829-
if (type.getTypeHandler() == null) {
830-
throw new IllegalArgumentException("Cannot find a deserializer for non-concrete Collection type "+type);
831-
}
832-
deser = AbstractDeserializer.constructForNonPOJO(beanDesc);
833-
} else {
827+
if (implType != null) {
834828
type = implType;
835829
// But if so, also need to re-check creators...
836830
beanDesc = config.introspectForCreation(type);
@@ -972,9 +966,9 @@ public JsonDeserializer<?> createMapDeserializer(DeserializationContext ctxt,
972966
*/
973967
if (deser == null) {
974968
if (type.isInterface() || type.isAbstract()) {
975-
MapType fallback = _mapAbstractMapType(type, config);
976-
if (fallback != null) {
977-
type = (MapType) fallback;
969+
MapType implType = _mapAbstractMapType(type, config);
970+
if (implType != null) {
971+
type = (MapType) implType;
978972
mapClass = type.getRawClass();
979973
// But if so, also need to re-check creators...
980974
beanDesc = config.introspectForCreation(type);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package com.fasterxml.jackson.databind.deser.merge;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
import com.fasterxml.jackson.annotation.JsonMerge;
9+
import com.fasterxml.jackson.annotation.JsonProperty;
10+
import com.fasterxml.jackson.databind.ObjectMapper;
11+
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
12+
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
13+
14+
import static org.junit.jupiter.api.Assertions.assertEquals;
15+
import static org.junit.jupiter.api.Assertions.assertTrue;
16+
import static org.junit.jupiter.api.Assertions.fail;
17+
18+
// [databind#4783] Test to verify that JsonMerge also works for custom list
19+
@SuppressWarnings("serial")
20+
public class CustomCollectionMerge4783Test
21+
extends DatabindTestUtil
22+
{
23+
static class MyArrayListJDK<T> extends ArrayList<T> { }
24+
25+
static class MergeListJDK {
26+
@JsonMerge
27+
@JsonProperty
28+
public List<String> values = new MyArrayListJDK<>();
29+
{ values.add("a");}
30+
}
31+
32+
interface MyListCustom<T> extends List<T> { }
33+
34+
static class MyArrayListCustom<T> extends ArrayList<T> implements MyListCustom<T> { }
35+
36+
static abstract class MyAbstractStringList extends ArrayList<String> {
37+
MyAbstractStringList() { super(); }
38+
MyAbstractStringList(int i) { super(); }
39+
}
40+
41+
static class MergeCustomStringList {
42+
@JsonMerge
43+
@JsonProperty
44+
public MyListCustom<String> values = new MyArrayListCustom<>();
45+
{ values.add("a"); }
46+
}
47+
48+
static class MergeMyCustomLongList {
49+
@JsonMerge
50+
@JsonProperty
51+
public MyListCustom<Long> values = new MyArrayListCustom<>();
52+
{ values.add(1L); }
53+
}
54+
55+
static class MergeMyCustomPojoList {
56+
@JsonMerge
57+
@JsonProperty
58+
public MyListCustom<CustomPojo> values = new MyArrayListCustom<>();
59+
{
60+
values.add(CustomPojo.create("a", 1));
61+
values.add(CustomPojo.create("b", 2));
62+
}
63+
}
64+
65+
// And then non-merging case too
66+
static class NonMergeCustomStringList {
67+
public MyListCustom<String> values;
68+
}
69+
70+
public static class CustomPojo {
71+
public String name;
72+
public int age;
73+
74+
public static CustomPojo create(String name, int age) {
75+
CustomPojo pojo = new CustomPojo();
76+
pojo.name = name;
77+
pojo.age = age;
78+
return pojo;
79+
}
80+
}
81+
82+
private final ObjectMapper MAPPER = newJsonMapper();
83+
84+
@Test
85+
void testJDKMapperReading() throws Exception {
86+
MergeListJDK result = MAPPER.readValue("{\"values\":[\"x\"]}", MergeListJDK.class);
87+
88+
assertEquals(2, result.values.size());
89+
assertTrue(result.values.contains("x"));
90+
assertTrue(result.values.contains("a"));
91+
}
92+
93+
@Test
94+
void testCustomMapperReading() throws Exception {
95+
MergeCustomStringList result = MAPPER.readValue("{\"values\":[\"x\"]}",
96+
MergeCustomStringList.class);
97+
98+
assertEquals(2, result.values.size());
99+
assertTrue(result.values.contains("x"));
100+
assertTrue(result.values.contains("a"));
101+
}
102+
103+
@Test
104+
void testCustomMapperReadingLongArrayList() throws Exception {
105+
MergeMyCustomLongList result = MAPPER.readValue("{\"values\":[7]}",
106+
MergeMyCustomLongList.class);
107+
108+
assertEquals(2, result.values.size());
109+
assertTrue(result.values.contains(1L));
110+
assertTrue(result.values.contains(7L));
111+
}
112+
113+
@Test
114+
void testCustomMapperReadingPojoArrayList() throws Exception {
115+
MergeMyCustomPojoList result = MAPPER.readValue("{\"values\":[{\"name\":\"c\",\"age\":3}]}",
116+
MergeMyCustomPojoList.class);
117+
118+
assertEquals(3, result.values.size());
119+
}
120+
121+
// // // And then failure cases
122+
123+
// Fail can't construct Collection interface unless there's maaping
124+
@Test
125+
void failNonMergeInterfaceList() throws Exception {
126+
try {
127+
MAPPER.readValue("{\"values\":[\"x\"]}", NonMergeCustomStringList.class);
128+
fail("Should not pass");
129+
} catch (InvalidDefinitionException e) {
130+
verifyException(e, String.format(
131+
"Cannot construct instance of `%s` (no Creators",
132+
MyListCustom.class.getName()));
133+
}
134+
}
135+
136+
// Fail can't construct abstract types
137+
@Test
138+
void failNonMergeAbstractList() throws Exception {
139+
try {
140+
MAPPER.readValue("[]", MyAbstractStringList.class);
141+
fail("Should not pass");
142+
} catch (InvalidDefinitionException e) {
143+
verifyException(e, String.format(
144+
"Cannot construct instance of `%s` (no Creators",
145+
MyAbstractStringList.class.getName()));
146+
}
147+
}
148+
}

0 commit comments

Comments
 (0)