Skip to content

Commit 125983f

Browse files
authored
Implement forward reference resolution for ObjectArrayDeserializer (2.x backport) (#5425)
1 parent 8e84b06 commit 125983f

File tree

5 files changed

+251
-5
lines changed

5 files changed

+251
-5
lines changed

release-notes/CREDITS-2.x

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1982,3 +1982,7 @@ Michael Reiche (@mikereiche)
19821982
Johnny Lim (@izeye)
19831983
* Reported #5293: Fix minor typo in `PropertyBindingException.getMessageSuffix()`
19841984
(2.21.0)
1985+
1986+
Hélios Gilles (@RoiSoleil)
1987+
* Contributed #5413: Add/support forward reference resolution for array values
1988+
[2.21.0]

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Project: jackson-databind
2323
anymore since 2.18.4
2424
(reported by @victor-noel-pfx)
2525
(fix by @cowtowncoder, w/ Claude code)
26+
#5413: Add/support forward reference resolution for array values
27+
(contributed by Hélios G)
2628

2729
2.20.2 (not yet released)
2830

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ public static class CollectionReferringAccumulator {
537537
/**
538538
* A list of {@link CollectionReferring} to maintain ordering.
539539
*/
540-
private List<CollectionReferring> _accumulator = new ArrayList<CollectionReferring>();
540+
private List<CollectionReferring> _accumulator = new ArrayList<>();
541541

542542
public CollectionReferringAccumulator(Class<?> elementType, Collection<Object> result) {
543543
_elementType = elementType;
@@ -591,7 +591,7 @@ public void resolveForwardReference(Object id, Object value) throws IOException
591591
*/
592592
private final static class CollectionReferring extends Referring {
593593
private final CollectionReferringAccumulator _parent;
594-
public final List<Object> next = new ArrayList<Object>();
594+
public final List<Object> next = new ArrayList<>();
595595

596596
CollectionReferring(CollectionReferringAccumulator parent,
597597
UnresolvedForwardReference reference, Class<?> contentType)

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

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,25 @@
22

33
import java.io.IOException;
44
import java.lang.reflect.Array;
5-
import java.util.Arrays;
6-
import java.util.Objects;
5+
import java.util.*;
76

87
import com.fasterxml.jackson.annotation.JsonFormat;
98

109
import com.fasterxml.jackson.core.*;
10+
1111
import com.fasterxml.jackson.databind.*;
1212
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
1313
import com.fasterxml.jackson.databind.cfg.CoercionAction;
1414
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
1515
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
1616
import com.fasterxml.jackson.databind.deser.NullValueProvider;
17+
import com.fasterxml.jackson.databind.deser.UnresolvedForwardReference;
18+
import com.fasterxml.jackson.databind.deser.impl.ReadableObjectId.Referring;
1719
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
1820
import com.fasterxml.jackson.databind.type.ArrayType;
1921
import com.fasterxml.jackson.databind.type.LogicalType;
2022
import com.fasterxml.jackson.databind.util.AccessPattern;
23+
import com.fasterxml.jackson.databind.util.ClassUtil;
2124
import com.fasterxml.jackson.databind.util.ObjectBuffer;
2225

2326
/**
@@ -196,10 +199,17 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt)
196199
if (!p.isExpectedStartArrayToken()) {
197200
return handleNonArray(p, ctxt);
198201
}
199-
202+
if (_elementDeserializer.getObjectIdReader() != null) {
203+
return _deserializeWithObjectId(p, ctxt);
204+
}
200205
final ObjectBuffer buffer = ctxt.leaseObjectBuffer();
201206
Object[] chunk = buffer.resetAndStart();
202207
int ix = 0;
208+
return _deserialize(p, ctxt, buffer, ix, chunk);
209+
}
210+
211+
protected Object[] _deserialize(JsonParser p, DeserializationContext ctxt,
212+
final ObjectBuffer buffer, int ix, Object[] chunk) throws JsonMappingException {
203213
JsonToken t;
204214

205215
try {
@@ -245,6 +255,54 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt)
245255
return result;
246256
}
247257

258+
protected Object[] _deserializeWithObjectId(JsonParser p, DeserializationContext ctxt) throws JsonMappingException {
259+
final ObjectArrayReferringAccumulator acc = new ObjectArrayReferringAccumulator(_untyped, _elementClass);
260+
261+
JsonToken t;
262+
263+
int ix = 0;
264+
try {
265+
while ((t = p.nextToken()) != JsonToken.END_ARRAY) {
266+
try {
267+
Object value;
268+
269+
if (t == JsonToken.VALUE_NULL) {
270+
if (_skipNullValues) {
271+
continue;
272+
}
273+
value = null;
274+
} else {
275+
value = _deserializeNoNullChecks(p, ctxt);
276+
}
277+
278+
if (value == null) {
279+
value = _nullProvider.getNullValue(ctxt);
280+
281+
if (value == null && _skipNullValues) {
282+
continue;
283+
}
284+
}
285+
acc.add(value);
286+
} catch (UnresolvedForwardReference reference) {
287+
if (acc == null) {
288+
throw reference;
289+
}
290+
ArrayReferring referring = new ArrayReferring(reference, _elementClass, acc);
291+
reference.getRoid().appendReferring(referring);
292+
}
293+
++ix;
294+
}
295+
} catch (Exception e) {
296+
boolean wrap = (ctxt == null) || ctxt.isEnabled(DeserializationFeature.WRAP_EXCEPTIONS);
297+
if (!wrap) {
298+
ClassUtil.throwIfRTE(e);
299+
}
300+
throw JsonMappingException.wrapWithPath(e, acc.buildArray(), ix);
301+
}
302+
303+
return acc.buildArray();
304+
}
305+
248306
@Override
249307
public Object deserializeWithType(JsonParser p, DeserializationContext ctxt,
250308
TypeDeserializer typeDeserializer)
@@ -425,4 +483,59 @@ protected Object _deserializeNoNullChecks(JsonParser p, DeserializationContext c
425483
}
426484
return _elementDeserializer.deserializeWithType(p, ctxt, _elementTypeDeserializer);
427485
}
486+
487+
// @since 2.21
488+
private static class ObjectArrayReferringAccumulator {
489+
private final boolean _untyped;
490+
private final Class<?> _elementType;
491+
private final List<Object> _accumulator = new ArrayList<>();
492+
493+
private Object[] _array;
494+
495+
ObjectArrayReferringAccumulator(boolean untyped, Class<?> elementType) {
496+
_untyped = untyped;
497+
_elementType = elementType;
498+
}
499+
500+
void add(Object value) {
501+
_accumulator.add(value);
502+
}
503+
504+
Object[] buildArray() {
505+
if (_untyped) {
506+
_array = new Object[_accumulator.size()];
507+
} else {
508+
_array = (Object[]) Array.newInstance(_elementType, _accumulator.size());
509+
}
510+
for (int i = 0; i < _accumulator.size(); i++) {
511+
if (!(_accumulator.get(i) instanceof ArrayReferring)) {
512+
_array[i] = _accumulator.get(i);
513+
}
514+
}
515+
return _array;
516+
}
517+
}
518+
519+
private static class ArrayReferring extends Referring {
520+
private final ObjectArrayReferringAccumulator _parent;
521+
522+
ArrayReferring(UnresolvedForwardReference ref,
523+
Class<?> type,
524+
ObjectArrayReferringAccumulator acc) {
525+
super(ref, type);
526+
_parent = acc;
527+
_parent._accumulator.add(this);
528+
}
529+
530+
@Override
531+
public void handleResolvedForwardReference(Object id, Object value) throws JacksonException {
532+
for (int i = 0; i < _parent._accumulator.size(); i++) {
533+
if (_parent._accumulator.get(i) == this) {
534+
_parent._array[i] = value;
535+
return;
536+
}
537+
}
538+
throw new IllegalArgumentException("Trying to resolve unknown reference: " + id);
539+
}
540+
}
428541
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package com.fasterxml.jackson.databind.objectid;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
6+
import com.fasterxml.jackson.annotation.JsonIdentityReference;
7+
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
10+
11+
import static org.junit.jupiter.api.Assertions.*;
12+
13+
/**
14+
* This unit test verifies that the "Native" java type mapper can properly deal with
15+
* "forward reference resolution" for values in Object arrays (not just
16+
* {@link java.util.Collection}s).
17+
*/
18+
public class ObjectIdInObjectArray5413Test extends DatabindTestUtil
19+
{
20+
static final class Draw {
21+
private Shape[] ashapes;
22+
private Point[] points;
23+
24+
public Shape[] getAShapes() {
25+
return ashapes;
26+
}
27+
28+
public void setAShapes(Shape[] shapes) {
29+
this.ashapes = shapes;
30+
}
31+
32+
public Point[] getPoints() {
33+
return points;
34+
}
35+
36+
public void setPoints(Point[] points) {
37+
this.points = points;
38+
}
39+
}
40+
41+
static final class Shape {
42+
@JsonIdentityReference(alwaysAsId = true)
43+
private Point[] points;
44+
45+
public Point[] getPoints() {
46+
return points;
47+
}
48+
49+
public void setPoints(Point[] points) {
50+
this.points = points;
51+
}
52+
}
53+
54+
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
55+
static final class Point {
56+
private int id;
57+
private int x;
58+
private int y;
59+
60+
public Point() {
61+
}
62+
63+
public Point(int id, int x, int y) {
64+
this.id = id;
65+
this.x = x;
66+
this.y = y;
67+
}
68+
69+
public int getId() {
70+
return id;
71+
}
72+
73+
public void setId(int id) {
74+
this.id = id;
75+
}
76+
77+
public int getX() {
78+
return x;
79+
}
80+
81+
public void setX(int x) {
82+
this.x = x;
83+
}
84+
85+
public int getY() {
86+
return y;
87+
}
88+
89+
public void setY(int y) {
90+
this.y = y;
91+
}
92+
}
93+
94+
private final ObjectMapper MAPPER = newJsonMapper();
95+
96+
// [databind#5413]
97+
@Test
98+
public void testForwardReferenceResolution() throws Exception
99+
{
100+
Draw draw = new Draw();
101+
Point point_0_0 = new Point(1, 0, 0);
102+
Point point_0_2 = new Point(2, 0, 2);
103+
Point point_2_2 = new Point(3, 2, 2);
104+
Point point_2_0 = new Point(4, 2, 0);
105+
Point point_1_3 = new Point(5, 1, 3);
106+
Shape square = new Shape();
107+
square.setPoints(new Point[] { point_0_0, point_0_2, point_2_2, point_2_0 });
108+
Shape triangle = new Shape();
109+
triangle.setPoints(new Point[] { point_0_2, point_1_3, point_2_2 });
110+
draw.setAShapes(new Shape[] { square, triangle });
111+
draw.setPoints(new Point[] { point_0_0, point_0_2, point_2_2, point_2_0, point_1_3 });
112+
final String JSON = MAPPER.writeValueAsString(draw);
113+
draw = MAPPER.readValue(JSON, Draw.class);
114+
assertNotNull(draw);
115+
assertEquals(5, draw.points.length);
116+
assertEquals(2, draw.ashapes.length);
117+
assertEquals(4, draw.ashapes[0].points.length);
118+
assertEquals(3, draw.ashapes[1].points.length);
119+
assertSame(draw.points[0], draw.ashapes[0].points[0]);
120+
assertSame(draw.points[1], draw.ashapes[0].points[1]);
121+
assertSame(draw.points[2], draw.ashapes[0].points[2]);
122+
assertSame(draw.points[3], draw.ashapes[0].points[3]);
123+
assertSame(draw.points[1], draw.ashapes[1].points[0]);
124+
assertSame(draw.points[4], draw.ashapes[1].points[1]);
125+
assertSame(draw.points[2], draw.ashapes[1].points[2]);
126+
}
127+
}

0 commit comments

Comments
 (0)