ObjectArrayDeserializer.java
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.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;
import tools.jackson.databind.type.LogicalType;
import tools.jackson.databind.util.AccessPattern;
import tools.jackson.databind.util.ObjectBuffer;
/**
* Serializer that can serialize non-primitive arrays.
*/
@JacksonStdImpl
public class ObjectArrayDeserializer
extends ContainerDeserializerBase<Object>
{
// // Configuration
/**
* Flag that indicates whether the component type is Object or not.
* Used for minor optimization when constructing result.
*/
protected final boolean _untyped;
/**
* Type of contained elements: needed for constructing actual
* result array
*/
protected final Class<?> _elementClass;
/**
* Element deserializer
*/
protected ValueDeserializer<Object> _elementDeserializer;
/**
* If element instances have polymorphic type information, this
* is the type deserializer that can handle it
*/
protected final TypeDeserializer _elementTypeDeserializer;
/**
* Zero-sized value of array type.
*/
protected final Object[] _emptyValue;
/*
/**********************************************************************
/* Life-cycle
/**********************************************************************
*/
public ObjectArrayDeserializer(JavaType arrayType0,
ValueDeserializer<Object> elemDeser, TypeDeserializer elemTypeDeser)
{
super(arrayType0, null, null);
ArrayType arrayType = (ArrayType) arrayType0;
_elementClass = arrayType.getContentType().getRawClass();
_untyped = (_elementClass == Object.class);
_elementDeserializer = elemDeser;
_elementTypeDeserializer = elemTypeDeser;
_emptyValue = arrayType.getEmptyArray();
}
protected ObjectArrayDeserializer(ObjectArrayDeserializer base,
ValueDeserializer<Object> elemDeser, TypeDeserializer elemTypeDeser,
NullValueProvider nuller, Boolean unwrapSingle)
{
super(base, nuller, unwrapSingle);
_elementClass = base._elementClass;
_untyped = base._untyped;
_emptyValue = base._emptyValue;
_elementDeserializer = elemDeser;
_elementTypeDeserializer = elemTypeDeser;
}
/**
* Overridable fluent-factory method used to create contextual instances
*/
public ObjectArrayDeserializer withDeserializer(TypeDeserializer elemTypeDeser,
ValueDeserializer<?> elemDeser)
{
return withResolved(elemTypeDeser, elemDeser,
_nullProvider, _unwrapSingle);
}
@SuppressWarnings("unchecked")
public ObjectArrayDeserializer withResolved(TypeDeserializer elemTypeDeser,
ValueDeserializer<?> elemDeser, NullValueProvider nuller, Boolean unwrapSingle)
{
if ((Objects.equals(unwrapSingle, _unwrapSingle)) && (nuller == _nullProvider)
&& (elemDeser == _elementDeserializer)
&& (elemTypeDeser == _elementTypeDeserializer)) {
return this;
}
return new ObjectArrayDeserializer(this,
(ValueDeserializer<Object>) elemDeser, elemTypeDeser,
nuller, unwrapSingle);
}
@Override
public boolean isCachable() {
// Important: do NOT cache if polymorphic values, or if there are annotation-based
// custom deserializers
return (_elementDeserializer == null) && (_elementTypeDeserializer == null);
}
@Override
public LogicalType logicalType() {
return LogicalType.Array;
}
@Override
public ValueDeserializer<?> createContextual(DeserializationContext ctxt,
BeanProperty property)
{
ValueDeserializer<?> valueDeser = _elementDeserializer;
// 07-May-2020, tatu: Is the argument `containerType.getRawClass()` right here?
// In a way seems like it should rather refer to value class... ?
// (as it's individual value of element type, not Container)...
Boolean unwrapSingle = findFormatFeature(ctxt, property, _containerType.getRawClass(),
JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
// May have a content converter
valueDeser = findConvertingContentDeserializer(ctxt, property, valueDeser);
final JavaType vt = _containerType.getContentType();
if (valueDeser == null) {
valueDeser = ctxt.findContextualValueDeserializer(vt, property);
} else { // if directly assigned, probably not yet contextual, so:
valueDeser = ctxt.handleSecondaryContextualization(valueDeser, property, vt);
}
TypeDeserializer elemTypeDeser = _elementTypeDeserializer;
if (elemTypeDeser != null) {
elemTypeDeser = elemTypeDeser.forProperty(property);
}
NullValueProvider nuller = findContentNullProvider(ctxt, property, valueDeser);
return withResolved(elemTypeDeser, valueDeser, nuller, unwrapSingle);
}
/*
/**********************************************************************
/* ContainerDeserializerBase API
/**********************************************************************
*/
@Override
public ValueDeserializer<Object> getContentDeserializer() {
return _elementDeserializer;
}
@Override
public AccessPattern getEmptyAccessPattern() {
// immutable, shareable so:
return AccessPattern.CONSTANT;
}
// need to override as we can't expose ValueInstantiator
@Override
public Object getEmptyValue(DeserializationContext ctxt) {
// 03-Jul-2020, tatu: Must be assignment-compatible; cannot just return `new Object[0]`
// if element type is different
return _emptyValue;
}
/*
/**********************************************************************
/* ValueDeserializer API
/**********************************************************************
*/
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt)
throws JacksonException
{
// Ok: must point to START_ARRAY (or equivalent)
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;
return _deserialize(p, ctxt, buffer, ix, chunk);
}
@Override
public Object deserializeWithType(JsonParser p, DeserializationContext ctxt,
TypeDeserializer typeDeserializer)
throws JacksonException
{
// Should there be separate handling for base64 stuff?
// for now this should be enough:
return (Object[]) typeDeserializer.deserializeTypedFromArray(p, ctxt);
}
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt,
Object intoValue0)
throws JacksonException
{
final Object[] intoValue = (Object[]) intoValue0;
if (!p.isExpectedStartArrayToken()) {
Object[] arr = (Object[]) handleNonArray(p, ctxt);
if (arr == null) {
return intoValue;
}
final int offset = intoValue.length;
Object[] result = Arrays.copyOf(intoValue, offset + arr.length);
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);
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;
}
value = null;
} 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;
}
final Object[] result;
if (_untyped) {
result = buffer.completeAndClearBuffer(chunk, ix);
} else {
result = buffer.completeAndClearBuffer(chunk, ix, _elementClass);
}
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();
}
/*
/**********************************************************************
/* Internal methods
/**********************************************************************
*/
protected Byte[] deserializeFromBase64(JsonParser p, DeserializationContext ctxt)
throws JacksonException
{
// First same as what PrimitiveArrayDeserializers.ByteDeser does:
byte[] b = p.getBinaryValue(ctxt.getBase64Variant());
// But then need to convert to wrappers
Byte[] result = new Byte[b.length];
for (int i = 0, len = b.length; i < len; ++i) {
result[i] = b[i];
}
return result;
}
protected Object handleNonArray(JsonParser p, DeserializationContext ctxt)
throws JacksonException
{
// Can we do implicit coercion to a single-element array still?
boolean canWrap = (_unwrapSingle == Boolean.TRUE) ||
((_unwrapSingle == null) &&
ctxt.isEnabled(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY));
if (!canWrap) {
// 2 exceptions with Strings:
if (p.hasToken(JsonToken.VALUE_STRING)) {
// One exception; byte arrays are generally serialized as base64, so that should be handled
// note: not `byte[]`, but `Byte[]` -- former is primitive array
if (_elementClass == Byte.class) {
return deserializeFromBase64(p, ctxt);
}
// Second: empty (and maybe blank) String
return _deserializeFromString(p, ctxt);
}
return ctxt.handleUnexpectedToken(_containerType, p);
}
JsonToken t = p.currentToken();
Object value;
if (t == JsonToken.VALUE_NULL) {
// 03-Feb-2017, tatu: Should this be skipped or not?
if (_skipNullValues) {
return _emptyValue;
}
value = null;
} else {
if (p.hasToken(JsonToken.VALUE_STRING)) {
String textValue = p.getString();
// https://github.com/FasterXML/jackson-dataformat-xml/issues/513
if (textValue.isEmpty()) {
final CoercionAction act = ctxt.findCoercionAction(logicalType(), handledType(),
CoercionInputShape.EmptyString);
if (act != CoercionAction.Fail) {
return (Object[]) _deserializeFromEmptyString(p, ctxt, act, handledType(),
"empty String (\"\")");
}
} else if (_isBlank(textValue)) {
final CoercionAction act = ctxt.findCoercionFromBlankString(logicalType(), handledType(),
CoercionAction.Fail);
if (act != CoercionAction.Fail) {
return (Object[]) _deserializeFromEmptyString(p, ctxt, act, handledType(),
"blank String (all whitespace)");
}
}
// if coercion failed, we can still add it to a list
}
value = _deserializeNoNullChecks(p, ctxt);
}
if (value == null) {
value = _nullProvider.getNullValue(ctxt);
if (value == null && _skipNullValues) {
return _emptyValue;
}
}
// Ok: bit tricky, since we may want T[], not just Object[]
Object[] result;
if (_untyped) {
result = new Object[1];
} else {
result = (Object[]) Array.newInstance(_elementClass, 1);
}
result[0] = value;
return result;
}
/**
* Deserialize the content of the map.
* If _elementTypeDeserializer is null, use _elementDeserializer.deserialize; if non-null,
* use _elementDeserializer.deserializeWithType to deserialize value.
* This method only performs deserialization and does not consider _skipNullValues, _nullProvider, etc.
*/
protected Object _deserializeNoNullChecks(JsonParser p, DeserializationContext ctxt)
throws JacksonException
{
if (_elementTypeDeserializer == null) {
return _elementDeserializer.deserialize(p, ctxt);
}
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);
}
}
}