Skip to content

Commit ff8c5a2

Browse files
authored
Fix #5237 (custom Map @JsonMerge fail) (#5251)
1 parent abe7a78 commit ff8c5a2

File tree

6 files changed

+137
-9
lines changed

6 files changed

+137
-9
lines changed

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Project: jackson-databind
66

77
Not yet released
88

9+
#5237: Failing `@JsonMerge` with a custom Map with a `@JsonCreator` constructor
10+
(reported by @nlisker)
911
#5242: Support "binary vectors": `@JsonFormat(shape = Shape.BINARY)` for
1012
`float[]`, `double[]`
1113

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,16 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri
476476
if ((prop != null) &&
477477
// [databind#3938]: except if it's MethodProperty
478478
(!_beanType.isRecordType() || (prop instanceof MethodProperty))) {
479+
480+
// 12-Aug-2025, tatu: [databind#5237] Mergeable properties need
481+
// special handling: must defer deserialization until POJO
482+
// is constructed.
483+
if (prop instanceof MergingSettableBeanProperty) {
484+
TokenBuffer tb = ctxt.bufferForInputBuffering(p);
485+
tb.copyCurrentStructure(p);
486+
buffer.bufferMergingProperty(prop, tb);
487+
continue;
488+
}
479489
try {
480490
buffer.bufferProperty(prop, _deserializeWithErrorWrapping(p, ctxt, prop));
481491
} catch (UnresolvedForwardReference reference) {
@@ -485,7 +495,7 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri
485495
BeanReferring referring = handleUnresolvedReference(ctxt,
486496
prop, buffer, reference);
487497
if (referrings == null) {
488-
referrings = new ArrayList<BeanReferring>();
498+
referrings = new ArrayList<>();
489499
}
490500
referrings.add(referring);
491501
}

src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyBasedCreator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ public Object build(DeserializationContext ctxt, PropertyValueBuffer buffer) thr
273273

274274
// Anything buffered?
275275
for (PropertyValue pv = buffer.buffered(); pv != null; pv = pv.next) {
276-
pv.assign(bean);
276+
pv.assign(ctxt, bean);
277277
}
278278
}
279279
return bean;

src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValue.java

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
import java.io.IOException;
44

5+
import com.fasterxml.jackson.core.JsonParser;
6+
import com.fasterxml.jackson.databind.DeserializationContext;
57
import com.fasterxml.jackson.databind.deser.SettableAnyProperty;
68
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
9+
import com.fasterxml.jackson.databind.util.TokenBuffer;
710

811
/**
912
* Base class for property values that need to be buffered during
@@ -24,12 +27,25 @@ protected PropertyValue(PropertyValue next, Object value)
2427
this.value = value;
2528
}
2629

30+
/**
31+
* @deprecated Since 2.20 use {@link #assign(DeserializationContext, Object)}
32+
*/
33+
@Deprecated // since 2.20
34+
public final void assign(Object bean) throws IOException {
35+
assign(null, bean);
36+
}
37+
2738
/**
2839
* Method called to assign stored value of this property to specified
2940
* bean instance
41+
*
42+
* @since 2.20
3043
*/
31-
public abstract void assign(Object bean)
32-
throws IOException;
44+
public void assign(DeserializationContext ctxt, Object bean)
45+
throws IOException
46+
{
47+
throw new UnsupportedOperationException();
48+
}
3349

3450
/**
3551
* Method called to assign stored value of this property to specified
@@ -40,7 +56,7 @@ public abstract void assign(Object bean)
4056
public void setValue(Object parameterObject)
4157
throws IOException
4258
{
43-
throw new UnsupportedOperationException("Should not be called by this type " + getClass().getName());
59+
throw new UnsupportedOperationException("Should not be called on type: " + getClass().getName());
4460
}
4561

4662
/*
@@ -66,7 +82,7 @@ public Regular(PropertyValue next, Object value,
6682
}
6783

6884
@Override
69-
public void assign(Object bean)
85+
public void assign(DeserializationContext ctxt, Object bean)
7086
throws IOException
7187
{
7288
_property.set(bean, value);
@@ -95,7 +111,7 @@ public Any(PropertyValue next, Object value,
95111
}
96112

97113
@Override
98-
public void assign(Object bean)
114+
public void assign(DeserializationContext ctxt, Object bean)
99115
throws IOException
100116
{
101117
_property.set(bean, _propertyName, value);
@@ -119,7 +135,7 @@ public Map(PropertyValue next, Object value, Object key)
119135

120136
@SuppressWarnings("unchecked")
121137
@Override
122-
public void assign(Object bean)
138+
public void assign(DeserializationContext ctxt, Object bean)
123139
throws IOException
124140
{
125141
((java.util.Map<Object,Object>) bean).put(_key, value);
@@ -148,7 +164,7 @@ public AnyParameter(PropertyValue next, Object value,
148164
}
149165

150166
@Override
151-
public void assign(Object bean)
167+
public void assign(DeserializationContext ctxt, Object bean)
152168
throws IOException
153169
{
154170
// do nothing, as we are not assigning to a bean
@@ -163,4 +179,35 @@ public void setValue(Object parameterObject)
163179
_property.set(parameterObject, _propertyName, value);
164180
}
165181
}
182+
183+
/**
184+
* Property value type used when merging values.
185+
*
186+
* @since 2.20
187+
*/
188+
final static class Merging
189+
extends PropertyValue
190+
{
191+
final SettableBeanProperty _property;
192+
193+
public Merging(PropertyValue next, TokenBuffer buffered,
194+
SettableBeanProperty prop)
195+
{
196+
super(next, buffered);
197+
_property = prop;
198+
}
199+
200+
@Override
201+
public void assign(DeserializationContext ctxt, Object bean)
202+
throws IOException
203+
{
204+
TokenBuffer buffered = (TokenBuffer) value;
205+
try (JsonParser p = buffered.asParser()) {
206+
p.nextToken();
207+
// !!! 12-Aug-2025, tatu: We need DeserializationContext...
208+
// but for testing just pass null for now.
209+
_property.deserializeAndSet(p, ctxt, bean);
210+
}
211+
}
212+
}
166213
}

src/main/java/com/fasterxml/jackson/databind/deser/impl/PropertyValueBuffer.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.fasterxml.jackson.databind.deser.SettableAnyProperty;
99
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
1010
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
11+
import com.fasterxml.jackson.databind.util.TokenBuffer;
1112

1213
/**
1314
* Simple container used for temporarily buffering a set of
@@ -398,4 +399,9 @@ public void bufferMapProperty(Object key, Object value) {
398399
public void bufferAnyParameterProperty(SettableAnyProperty prop, String propName, Object value) {
399400
_anyParamBuffered = new PropertyValue.AnyParameter(_anyParamBuffered, value, prop, propName);
400401
}
402+
403+
// @since 2.20
404+
public void bufferMergingProperty(SettableBeanProperty prop, TokenBuffer buffered) {
405+
_buffered = new PropertyValue.Merging(_buffered, buffered, prop);
406+
}
401407
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.fasterxml.jackson.databind.deser.merge;
2+
3+
import java.util.*;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import com.fasterxml.jackson.annotation.JsonCreator;
8+
import com.fasterxml.jackson.annotation.JsonMerge;
9+
import com.fasterxml.jackson.annotation.JsonProperty;
10+
11+
import com.fasterxml.jackson.databind.ObjectMapper;
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.assertNotNull;
16+
17+
@SuppressWarnings("serial")
18+
public class CustomMapMerge5237Test
19+
extends DatabindTestUtil
20+
{
21+
// [databind#5237]
22+
interface MyMap<K, V> extends Map<K, V> {}
23+
24+
static class MapImpl<K, V> extends HashMap<K, V> implements MyMap<K, V> {}
25+
26+
static class MergeMap {
27+
int inter;
28+
String s;
29+
30+
@JsonMerge
31+
public MyMap<Integer, String> map = new MapImpl<>();
32+
33+
@JsonCreator
34+
MergeMap(@JsonProperty("inter") int inter, @JsonProperty("s") String s) {
35+
this.inter = inter;
36+
this.s = s;
37+
}
38+
39+
public int getInter() {
40+
return inter;
41+
}
42+
}
43+
44+
private final ObjectMapper MAPPER = newJsonMapper();
45+
46+
// [databind#5237]: Merge for custom maps fails
47+
@Test
48+
void customMapMerging5237() throws Exception
49+
{
50+
String json = "{\n"
51+
+ " \"inter\" : 5,\n"
52+
+ " \"map\" : {\n"
53+
+ " \"3\" : \"ADS\"\n"
54+
+ " },\n"
55+
+ " \"s\" : \"abc\"\n"
56+
+ "}";
57+
MergeMap merge2 = MAPPER.readValue(json, MergeMap.class);
58+
assertNotNull(merge2);
59+
assertEquals(Collections.singletonMap(3, "ADS"), merge2.map);
60+
assertEquals(5, merge2.getInter());
61+
assertEquals("abc", merge2.s);
62+
}
63+
}

0 commit comments

Comments
 (0)