Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.fasterxml.jackson.annotation.JsonInclude;

import tools.jackson.core.*;
import tools.jackson.core.type.WritableTypeId;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JacksonStdImpl;
import tools.jackson.databind.introspect.AnnotatedMember;
Expand Down Expand Up @@ -272,6 +273,47 @@ public final void serialize(Object[] value, JsonGenerator g, SerializationContex
g.writeEndArray();
}

// [databind#3194]: Override to check whether element type info should be
// written after the outer array's type ID has been written. When the outer
// type ID determines a concrete element type that is "final" (per default
// typing rules), inner element type IDs are redundant and cause
// deserialization failures.
@Override
public void serializeWithType(Object[] value, JsonGenerator g, SerializationContext ctxt,
TypeSerializer typeSer)
throws JacksonException
{
WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, ctxt,
typeSer.typeId(value, JsonToken.START_ARRAY));
g.assignCurrentValue(value);
if (_valueTypeSerializer != null && _elementSerializer == null
&& _shouldSkipElementTypeId(value, ctxt)) {
_serializeDynamicContents(value, g, ctxt);
} else {
serializeContents(value, g, ctxt);
}
typeSer.writeTypeSuffix(g, ctxt, typeIdDef);
}

/**
* Helper method for [databind#3194]: checks if the actual runtime array
* element type does NOT require type serialization based on default typing
* rules. For example, {@code String[]} has final component type {@code String},
* so with {@code NON_FINAL} typing it does not need a type wrapper.
*/
private boolean _shouldSkipElementTypeId(Object[] value, SerializationContext ctxt) {
Class<?> actualComponentType = value.getClass().getComponentType();
// If actual component type matches declared element type, definitely
// need type info (it was already determined to need it at construction time)
if (actualComponentType == _elementType.getRawClass()) {
return false;
}
// Check if the actual component type would get a TypeSerializer;
// if not, its type info is redundant since the outer type ID already
// determines the concrete element type.
return ctxt.findTypeSerializer(ctxt.constructType(actualComponentType)) == null;
}

@Override
public void serializeContents(Object[] value, JsonGenerator g, SerializationContext ctxt)
throws JacksonException
Expand All @@ -288,6 +330,23 @@ public void serializeContents(Object[] value, JsonGenerator g, SerializationCont
serializeTypedContents(value, g, ctxt);
return;
}
_serializeDynamicContents(value, g, ctxt);
}

/**
* Serialize array elements using dynamically resolved per-element serializers,
* WITHOUT element type wrappers.
* Extracted from {@link #serializeContents} for reuse from
* {@link #serializeWithType} (see [databind#3194]).
*/
protected void _serializeDynamicContents(Object[] value, JsonGenerator g,
SerializationContext ctxt)
throws JacksonException
{
final int len = value.length;
if (len == 0) {
return;
}
final boolean filtered = _needToCheckFiltering(ctxt);
int i = 0;
Object elem = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package tools.jackson.databind.jsontype;

import org.junit.jupiter.api.Test;

import tools.jackson.databind.DefaultTyping;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;

// [databind#3194]: Discrepancy between Type Id inclusion on serialization vs
// expectation during deserialization of 2D arrays of final types when using
// DefaultTyping.NON_FINAL
class PolymorphicArrays3194Test extends DatabindTestUtil {
static final class SomeBean {
public Object[][] value;
}

private final ObjectMapper MAPPER = JsonMapper
.builder()
.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfSubTypeIsArray()
.allowIfSubType(Object.class)
.build(),
DefaultTyping.NON_FINAL)
.build();

// Case 1: Object[][] containing String[] elements (explicit String[] subarrays)
@Test
void twoDimensionalArrayObjectWithStringElements() throws Exception {
SomeBean instance = new SomeBean();
instance.value = new Object[][]{new String[]{"1.1", "1.2"}, new String[]{"2.1", "2.2"}};
String json = MAPPER.writeValueAsString(instance);

SomeBean result = MAPPER.readValue(json, SomeBean.class);
assertEquals(Object[][].class, result.value.getClass());
assertEquals(String[].class, result.value[0].getClass());
assertArrayEquals(new String[]{"1.1", "1.2"}, (String[]) result.value[0]);
assertArrayEquals(new String[]{"2.1", "2.2"}, (String[]) result.value[1]);
}

// Case 2: Object[][] containing Object[] elements
@Test
void twoDimensionalArrayObjectWithObjectElements() throws Exception {
SomeBean instance = new SomeBean();
instance.value = new Object[][]{{"1.1", "1.2"}, {"2.1", "2.2"}};
String json = MAPPER.writeValueAsString(instance);

SomeBean result = MAPPER.readValue(json, SomeBean.class);
assertEquals(Object[][].class, result.value.getClass());
assertEquals(Object[].class, result.value[0].getClass());
}

// Case 3 (the original failing case): String[][] stored in Object[][]
@Test
void twoDimensionalStringArrayInObjectField() throws Exception {
SomeBean instance = new SomeBean();
instance.value = new String[][]{{"1.1", "1.2"}, {"2.1", "2.2"}};
String json = MAPPER.writeValueAsString(instance);

// After the fix, inner arrays should NOT have type IDs since the outer
// type ID "[[Ljava.lang.String;" already determines element type String[]
// (which is final).
SomeBean result = MAPPER.readValue(json, SomeBean.class);
assertEquals(String[][].class, result.value.getClass());
assertEquals(String[].class, result.value[0].getClass());
assertArrayEquals(new String[]{"1.1", "1.2"}, result.value[0]);
assertArrayEquals(new String[]{"2.1", "2.2"}, result.value[1]);
}

// Case 4: Integer[][] stored in Object[][]
@Test
void twoDimensionalIntegerArrayInObjectField() throws Exception {
SomeBean instance = new SomeBean();
instance.value = new Integer[][]{{1, 2}, {3, 4}};
String json = MAPPER.writeValueAsString(instance);

SomeBean result = MAPPER.readValue(json, SomeBean.class);
assertEquals(Integer[][].class, result.value.getClass());
assertEquals(Integer[].class, result.value[0].getClass());
assertArrayEquals(new Integer[]{1, 2}, result.value[0]);
assertArrayEquals(new Integer[]{3, 4}, result.value[1]);
}

// Case 5: handcrafted JSON without inner type IDs (should still work)
@Test
void twoDimensionalStringArrayFromHandcraftedJson() throws Exception {
String handcrafted = "{\"value\":[\"[[Ljava.lang.String;\",[[\"1.1\",\"1.2\"],[\"2.1\",\"2.2\"]]]}";
SomeBean result = MAPPER.readValue(handcrafted, SomeBean.class);
assertEquals(String[][].class, result.value.getClass());
assertEquals(String[].class, result.value[0].getClass());
assertArrayEquals(new String[]{"1.1", "1.2"}, result.value[0]);
assertArrayEquals(new String[]{"2.1", "2.2"}, result.value[1]);
}
}

This file was deleted.