Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,7 @@ Fouad Almalki (@Eng-Fouad)
* Contributed fix for #5416: Annotations on an interface that's part of a JDK proxy are not honored
with Jackson 3
[3.0.3]

Hélios Gilles (@RoiSoleil)
* Contributed #5413: Add/support forward reference resolution for array values
[3.1.0]
2 changes: 2 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Versions: 3.x (for earlier see VERSION-2.x)
#5361: Fix Maven SBOM publishing (worked in 3.0.0-rc4 but not in rc5 or later)
(date-time)#359: `InstantDeserializer` deserializes the nanosecond portion of
fractional negative timestamps incorrectly
#5413: Add/support forward reference resolution for array values
(contributed by Hélios G)

3.0.3 (not yet released)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ public static class CollectionReferringAccumulator {
/**
* A list of {@link CollectionReferring} to maintain ordering.
*/
private List<CollectionReferring> _accumulator = new ArrayList<CollectionReferring>();
private List<CollectionReferring> _accumulator = new ArrayList<>();

public CollectionReferringAccumulator(Class<?> elementType, Collection<Object> result) {
_elementType = elementType;
Expand Down Expand Up @@ -581,7 +581,7 @@ public void resolveForwardReference(DeserializationContext ctxt, Object id, Obje
*/
private final static class CollectionReferring extends Referring {
private final CollectionReferringAccumulator _parent;
public final List<Object> next = new ArrayList<Object>();
public final List<Object> next = new ArrayList<>();

CollectionReferring(CollectionReferringAccumulator parent,
UnresolvedForwardReference reference, Class<?> contentType)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
package tools.jackson.databind.deser.jdk;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonFormat;

import tools.jackson.core.*;
import tools.jackson.databind.*;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.databind.BeanProperty;
import tools.jackson.databind.DatabindException;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.databind.annotation.JacksonStdImpl;
import tools.jackson.databind.cfg.CoercionAction;
import tools.jackson.databind.cfg.CoercionInputShape;
import tools.jackson.databind.deser.NullValueProvider;
import tools.jackson.databind.deser.ReadableObjectId.Referring;
import tools.jackson.databind.deser.UnresolvedForwardReference;
import tools.jackson.databind.deser.std.ContainerDeserializerBase;
import tools.jackson.databind.jsontype.TypeDeserializer;
import tools.jackson.databind.type.ArrayType;
Expand Down Expand Up @@ -188,54 +198,13 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt)
if (!p.isExpectedStartArrayToken()) {
return handleNonArray(p, ctxt);
}

if (_elementDeserializer.getObjectIdReader(ctxt) != null) {
return _deserializeWithObjectId(p, ctxt);
}
final ObjectBuffer buffer = ctxt.leaseObjectBuffer();
Object[] chunk = buffer.resetAndStart();
int ix = 0;
JsonToken t;

try {
while ((t = p.nextToken()) != JsonToken.END_ARRAY) {
// Note: must handle null explicitly here; value deserializers won't
Object value;

if (t == JsonToken.VALUE_NULL) {
if (_skipNullValues) {
continue;
}
value = null;
} else {
value = _deserializeNoNullChecks(p, ctxt);
}

if (value == null) {
value = _nullProvider.getNullValue(ctxt);

if (value == null && _skipNullValues) {
continue;
}
}

if (ix >= chunk.length) {
chunk = buffer.appendCompletedChunk(chunk);
ix = 0;
}
chunk[ix++] = value;
}
} catch (Exception e) {
throw DatabindException.wrapWithPath(ctxt, e,
new JacksonException.Reference(chunk, buffer.bufferedSize() + ix));
}

Object[] result;

if (_untyped) {
result = buffer.completeAndClearBuffer(chunk, ix);
} else {
result = buffer.completeAndClearBuffer(chunk, ix, _elementClass);
}
ctxt.returnObjectBuffer(buffer);
return result;
return _deserialize(p, ctxt, buffer, ix, chunk);
}

@Override
Expand Down Expand Up @@ -264,16 +233,23 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt,
System.arraycopy(arr, 0, result, offset, arr.length);
return result;
}
if (_elementDeserializer.getObjectIdReader(ctxt) != null) {
return _deserializeWithObjectId(p, ctxt);
}

final ObjectBuffer buffer = ctxt.leaseObjectBuffer();
int ix = intoValue.length;
Object[] chunk = buffer.resetAndStart(intoValue, ix);
JsonToken t;

try {
while ((t = p.nextToken()) != JsonToken.END_ARRAY) {
Object value;
return _deserialize(p, ctxt, buffer, ix, chunk);
}

protected Object[] _deserialize(JsonParser p, DeserializationContext ctxt,
final ObjectBuffer buffer, int ix, Object[] chunk)
{
JsonToken t;
while ((t = p.nextToken()) != JsonToken.END_ARRAY) {
Object value;
try {
if (t == JsonToken.VALUE_NULL) {
if (_skipNullValues) {
continue;
Expand All @@ -282,28 +258,25 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt,
} else {
value = _deserializeNoNullChecks(p, ctxt);
}

if (value == null) {
value = _nullProvider.getNullValue(ctxt);

if (value == null && _skipNullValues) {
continue;
}
}
} catch (Exception e) {
throw DatabindException.wrapWithPath(ctxt, e,
new JacksonException.Reference(chunk, buffer.bufferedSize() + ix));
}

if (ix >= chunk.length) {
chunk = buffer.appendCompletedChunk(chunk);
ix = 0;
}
chunk[ix++] = value;
if (ix >= chunk.length) {
chunk = buffer.appendCompletedChunk(chunk);
ix = 0;
}
} catch (Exception e) {
throw DatabindException.wrapWithPath(ctxt, e,
new JacksonException.Reference(chunk, buffer.bufferedSize() + ix));
chunk[ix++] = value;
}

Object[] result;

final Object[] result;
if (_untyped) {
result = buffer.completeAndClearBuffer(chunk, ix);
} else {
Expand All @@ -312,6 +285,51 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt,
ctxt.returnObjectBuffer(buffer);
return result;
}

protected Object[] _deserializeWithObjectId(JsonParser p, DeserializationContext ctxt)
{
final ObjectArrayReferringAccumulator acc = new ObjectArrayReferringAccumulator(_untyped, _elementClass);

JsonToken t;

int ix = 0;
while ((t = p.nextToken()) != JsonToken.END_ARRAY) {
try {
Object value;

if (t == JsonToken.VALUE_NULL) {
if (_skipNullValues) {
continue;
}
value = null;
} else {
value = _deserializeNoNullChecks(p, ctxt);
}

if (value == null) {
value = _nullProvider.getNullValue(ctxt);

if (value == null && _skipNullValues) {
continue;
}
}
acc.add(value);
} catch (UnresolvedForwardReference reference) {
if (acc == null) {
throw reference;
}
ArrayReferring referring = new ArrayReferring(reference, _elementClass, acc);
reference.getRoid().appendReferring(referring);
} catch (Exception e) {
throw DatabindException.wrapWithPath(ctxt, e,
// 22-Nov-2025, tatu: Not ideal but has to do
new JacksonException.Reference(acc.buildArray(), ix));
}
++ix;
}

return acc.buildArray();
}

/*
/**********************************************************************
Expand Down Expand Up @@ -420,4 +438,60 @@ protected Object _deserializeNoNullChecks(JsonParser p, DeserializationContext c
}
return _elementDeserializer.deserializeWithType(p, ctxt, _elementTypeDeserializer);
}

// @since 3.1
private static class ObjectArrayReferringAccumulator {
private final boolean _untyped;
private final Class<?> _elementType;
private final List<Object> _accumulator = new ArrayList<>();

private Object[] _array;

ObjectArrayReferringAccumulator(boolean untyped, Class<?> elementType) {
_untyped = untyped;
_elementType = elementType;
}

void add(Object value) {
_accumulator.add(value);
}

Object[] buildArray() {
if (_untyped) {
_array = new Object[_accumulator.size()];
} else {
_array = (Object[]) Array.newInstance(_elementType, _accumulator.size());
}
for (int i = 0; i < _accumulator.size(); i++) {
if (!(_accumulator.get(i) instanceof ArrayReferring)) {
_array[i] = _accumulator.get(i);
}
}
return _array;
}
}

private static class ArrayReferring extends Referring {
private final ObjectArrayReferringAccumulator _parent;

ArrayReferring(UnresolvedForwardReference ref,
Class<?> type,
ObjectArrayReferringAccumulator acc) {
super(ref, type);
_parent = acc;
_parent._accumulator.add(this);
}

@Override
public void handleResolvedForwardReference(DeserializationContext ctxt,
Object id, Object value) throws JacksonException {
for (int i = 0; i < _parent._accumulator.size(); i++) {
if (_parent._accumulator.get(i) == this) {
_parent._array[i] = value;
return;
}
}
throw new IllegalArgumentException("Trying to resolve unknown reference: " + id);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package tools.jackson.databind.objectid;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonIdentityReference;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;

/**
* This unit test verifies that the "Native" java type mapper can properly deal with
* "forward reference resolution" for values in Object arrays (not just
* {@link java.util.Collection}s).
*/
public class ObjectIdInObjectArray5413Test extends DatabindTestUtil
{
static final class Draw {
private Shape[] ashapes;
private Point[] points;

public Shape[] getAShapes() {
return ashapes;
}

public void setAShapes(Shape[] shapes) {
this.ashapes = shapes;
}

public Point[] getPoints() {
return points;
}

public void setPoints(Point[] points) {
this.points = points;
}
}

static final class Shape {
@JsonIdentityReference(alwaysAsId = true)
private Point[] points;

public Point[] getPoints() {
return points;
}

public void setPoints(Point[] points) {
this.points = points;
}
}

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public static final record Point(int id, int x, int y) {
}

private final ObjectMapper MAPPER = newJsonMapper();

// [databind#5413]
@Test
public void testForwardReferenceResolution()
{
Draw draw = new Draw();
Point point_0_0 = new Point(1, 0, 0);
Point point_0_2 = new Point(2, 0, 2);
Point point_2_2 = new Point(3, 2, 2);
Point point_2_0 = new Point(4, 2, 0);
Point point_1_3 = new Point(5, 1, 3);
Shape square = new Shape();
square.setPoints(new Point[] { point_0_0, point_0_2, point_2_2, point_2_0 });
Shape triangle = new Shape();
triangle.setPoints(new Point[] { point_0_2, point_1_3, point_2_2 });
draw.setAShapes(new Shape[] { square, triangle });
draw.setPoints(new Point[] { point_0_0, point_0_2, point_2_2, point_2_0, point_1_3 });
final String JSON = MAPPER.writeValueAsString(draw);
draw = MAPPER.readValue(JSON, Draw.class);
assertNotNull(draw);
assertEquals(5, draw.points.length);
assertEquals(2, draw.ashapes.length);
assertEquals(4, draw.ashapes[0].points.length);
assertEquals(3, draw.ashapes[1].points.length);
assertSame(draw.points[0], draw.ashapes[0].points[0]);
assertSame(draw.points[1], draw.ashapes[0].points[1]);
assertSame(draw.points[2], draw.ashapes[0].points[2]);
assertSame(draw.points[3], draw.ashapes[0].points[3]);
assertSame(draw.points[1], draw.ashapes[1].points[0]);
assertSame(draw.points[4], draw.ashapes[1].points[1]);
assertSame(draw.points[2], draw.ashapes[1].points[2]);
}

}