Skip to content

Commit b696d40

Browse files
authored
Fix #556: explicit fail on nested array/Collection writes (#858)
1 parent 2146e3f commit b696d40

5 files changed

Lines changed: 168 additions & 44 deletions

File tree

release-notes/VERSION

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ Version: 3.x (for earlier see VERSION-2.x)
8484
(fix by @cowtowncoder, w/ Claude code)
8585
#541: Allow specifying URI of the default namespace for root element
8686
(fix by @cowtowncoder, w/ Claude code)
87+
#556: `XmlMapper` does not support multi-dimensional arrays
88+
(reported by @schwarfl)
89+
(fix by @cowtowncoder, w/ Claude code)
8790
#561: Deserializing empty timestamp fields to `null` value doesn't
8891
work (instead becomes "empty", Epoch time)
8992
(reported by @silvestr85)

src/main/java/tools/jackson/dataformat/xml/XmlWriteFeature.java

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,59 @@
1111
*/
1212
public enum XmlWriteFeature implements FormatFeature
1313
{
14+
/**
15+
* Feature that enables automatic conversion of logical property
16+
* name {@code "xsi:type"} into matching XML name where "type"
17+
* is the local name and "xsi" prefix is bound to URI
18+
* {@link XMLConstants#W3C_XML_SCHEMA_INSTANCE_NS_URI},
19+
* and output is indicated to be done as XML Attribute.
20+
* This is mostly desirable for Polymorphic handling where it is difficult
21+
* to specify XML Namespace for type identifier
22+
*<p>
23+
* Default setting is {@code true} (enabled) in Jackson 3.0:
24+
* it was {@code false} (disabled)in Jackson 2.x.
25+
*/
26+
AUTO_DETECT_XSI_TYPE(true),
27+
28+
/**
29+
* Feature that determines whether attempt to serialize a directly nested
30+
* array or {@link java.util.Collection} (i.e. an
31+
* array/Collection whose immediate parent is also an array/Collection
32+
* — without an intermediate POJO) should fail with an exception (true)
33+
* or be allowed (false) — knowing that, when allowed, the resulting XML
34+
* cannot represent the nested structure and the inner dimension will be
35+
* silently flattened into the outer one, nor can it be deserialized back.
36+
*<p>
37+
* "Natural-style" XML has no canonical representation for an unnamed
38+
* nested array, so a clean round-trip is not possible without an
39+
* intermediate POJO wrapper.
40+
*<p>
41+
* Default setting is {@code true} (enabled): nested arrays cause an
42+
* exception. Disabling restores the legacy behavior of
43+
* silently flattening dimensions.
44+
*<p>
45+
* See <a href="https://github.com/FasterXML/jackson-dataformat-xml/issues/556">#556</a>.
46+
*
47+
* @since 3.2
48+
*/
49+
FAIL_ON_NESTED_ARRAYS(true),
50+
51+
/**
52+
* Feature that determines writing of root values of type {@code ObjectNode}
53+
* ({@code JsonNode} subtype that represents Object content values),
54+
* regarding XML output.
55+
* If enabled and {@code ObjectNode} has exactly one entry (key/value pair),
56+
* then key of that entry is used as the root element name (and value
57+
* is written as contents. Otherwise (if feature disabled, or if root
58+
* {@code ObjectNode} has any other number of key/value entries,
59+
* root element name is determined using normal logic (either explicitly
60+
* configured, or {@code ObjectNode} otherwise).
61+
*<p>
62+
* Default setting is {@code true} (enabled) in Jackson 3.x:
63+
* it was {@code false} (disabled)in Jackson 2.x.
64+
*/
65+
UNWRAP_ROOT_OBJECT_NODE(true),
66+
1467
/**
1568
* Feature that controls whether XML declaration should be written before
1669
* when generator is initialized (true) or not (false)
@@ -48,36 +101,6 @@ public enum XmlWriteFeature implements FormatFeature
48101
*/
49102
WRITE_NULLS_AS_XSI_NIL(true),
50103

51-
/**
52-
* Feature that determines writing of root values of type {@code ObjectNode}
53-
* ({@code JsonNode} subtype that represents Object content values),
54-
* regarding XML output.
55-
* If enabled and {@code ObjectNode} has exactly one entry (key/value pair),
56-
* then key of that entry is used as the root element name (and value
57-
* is written as contents. Otherwise (if feature disabled, or if root
58-
* {@code ObjectNode} has any other number of key/value entries,
59-
* root element name is determined using normal logic (either explicitly
60-
* configured, or {@code ObjectNode} otherwise).
61-
*<p>
62-
* Default setting is {@code true} (enabled) in Jackson 3.x:
63-
* it was {@code false} (disabled)in Jackson 2.x.
64-
*/
65-
UNWRAP_ROOT_OBJECT_NODE(true),
66-
67-
/**
68-
* Feature that enables automatic conversion of logical property
69-
* name {@code "xsi:type"} into matching XML name where "type"
70-
* is the local name and "xsi" prefix is bound to URI
71-
* {@link XMLConstants#W3C_XML_SCHEMA_INSTANCE_NS_URI},
72-
* and output is indicated to be done as XML Attribute.
73-
* This is mostly desirable for Polymorphic handling where it is difficult
74-
* to specify XML Namespace for type identifier
75-
*<p>
76-
* Default setting is {@code true} (enabled) in Jackson 3.0:
77-
* it was {@code false} (disabled)in Jackson 2.x.
78-
*/
79-
AUTO_DETECT_XSI_TYPE(true),
80-
81104
/**
82105
* Feature that determines how floating-point infinity values are
83106
* serialized.
@@ -104,7 +127,7 @@ public enum XmlWriteFeature implements FormatFeature
104127
* {@link tools.jackson.dataformat.xml.XmlReadFeature}.
105128
*<p>
106129
* Default setting is {@code true} (enabled) in Jackson 3.0:
107-
* it was {@code false} (disabled)in Jackson 2.x.
130+
* it was {@code false} (disabled) in Jackson 2.x.
108131
*/
109132
WRITE_XML_SCHEMA_CONFORMING_FLOATS(true),
110133
;

src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,7 @@ public WritableTypeId writeTypePrefix(WritableTypeId typeIdDef) throws JacksonEx
651651
public JsonGenerator writeStartArray() throws JacksonException
652652
{
653653
_verifyValueWrite("start an array");
654+
_verifyNotNestedArray();
654655
_streamWriteContext = _streamWriteContext.createChildArrayContext(null);
655656
streamWriteConstraints().validateNestingDepth(_streamWriteContext.getNestingDepth());
656657
if (_xmlPrettyPrinter != null) {
@@ -660,21 +661,37 @@ public JsonGenerator writeStartArray() throws JacksonException
660661
}
661662
return this;
662663
}
663-
664+
664665
@Override
665666
public JsonGenerator writeStartArray(Object currValue) throws JacksonException
666667
{
667668
_verifyValueWrite("start an array");
669+
_verifyNotNestedArray();
668670
_streamWriteContext = _streamWriteContext.createChildArrayContext(currValue);
669671
streamWriteConstraints().validateNestingDepth(_streamWriteContext.getNestingDepth());
670672
if (_xmlPrettyPrinter != null) {
671673
_xmlPrettyPrinter.writeStartArray(this);
672674
} else {
673-
// nothing to do here; no-operation
675+
// nothing to do here; no-op
674676
}
675677
return this;
676678
}
677679

680+
// [dataformat-xml#556]: nested arrays/Collections/Maps cannot be expressed
681+
// in natural-style XML without an intermediate POJO. Fail fast (when the
682+
// feature is enabled) instead of silently flattening dimensions.
683+
//
684+
// @since 3.2
685+
private void _verifyNotNestedArray() throws JacksonException
686+
{
687+
if (_streamWriteContext.inArray()
688+
&& XmlWriteFeature.FAIL_ON_NESTED_ARRAYS.enabledIn(_formatFeatures)) {
689+
_reportError("XML format does not support nested arrays/Collections;"
690+
+ " wrap inner array in a POJO"
691+
+ " (disable XmlWriteFeature.FAIL_ON_NESTED_ARRAYS to allow legacy flattening)");
692+
}
693+
}
694+
678695
@Override
679696
public JsonGenerator writeEndArray() throws JacksonException
680697
{
Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package tools.jackson.dataformat.xml.dos;
22

3-
import java.util.ArrayList;
4-
import java.util.List;
3+
import java.util.HashMap;
4+
import java.util.Map;
55

66
import org.junit.jupiter.api.Test;
77

@@ -17,26 +17,29 @@
1717

1818
/**
1919
* Simple unit tests to verify that we fail gracefully if you attempt to serialize
20-
* data that is cyclic (eg a list that contains itself).
20+
* data that is cyclic (eg a map that contains itself).
2121
*/
2222
public class CyclicXMLDataSerTest extends XmlTestUtil
2323
{
2424
private final XmlMapper MAPPER = newMapper();
2525

2626
@Test
27-
public void testListWithSelfReference() throws Exception {
28-
// Avoid direct loop as serializer might be able to catch
29-
List<Object> list1 = new ArrayList<>();
30-
List<Object> list2 = new ArrayList<>();
31-
list1.add(list2);
32-
list2.add(list1);
27+
public void testMapWithSelfReference() throws Exception {
28+
// Use Maps (rather than Lists) so the cycle exercises object-nesting
29+
// depth without tripping XmlWriteFeature.FAIL_ON_NESTED_ARRAYS.
30+
// Two distinct Maps avoid trivial self-loop detection.
31+
Map<String, Object> map1 = new HashMap<>();
32+
Map<String, Object> map2 = new HashMap<>();
33+
map1.put("ref", map2);
34+
map2.put("ref", map1);
3335
try {
34-
MAPPER.writeValueAsString(list1);
36+
MAPPER.writeValueAsString(map1);
3537
fail("expected DatabindException for infinite recursion");
3638
} catch (DatabindException e) {
3739
String exceptionPrefix = String.format("Document nesting depth (%d) exceeds the maximum allowed",
3840
StreamWriteConstraints.DEFAULT_MAX_DEPTH + 1);
39-
assertTrue(e.getMessage().startsWith(exceptionPrefix));
41+
assertTrue(e.getMessage().startsWith(exceptionPrefix),
42+
"Unexpected message: " + e.getMessage());
4043
}
4144
}
4245
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package tools.jackson.dataformat.xml.ser;
2+
3+
import java.util.Arrays;
4+
import java.util.List;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
import tools.jackson.core.JacksonException;
9+
import tools.jackson.dataformat.xml.XmlMapper;
10+
import tools.jackson.dataformat.xml.XmlTestUtil;
11+
import tools.jackson.dataformat.xml.XmlWriteFeature;
12+
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.junit.jupiter.api.Assertions.assertNotNull;
15+
import static org.junit.jupiter.api.Assertions.fail;
16+
17+
// [dataformat-xml#556]: nested arrays/Collections cannot be represented in
18+
// natural-style XML without an intermediate POJO; XmlMapper currently fails
19+
// fast on serialization rather than silently flattening dimensions.
20+
public class MultidimArray556Test extends XmlTestUtil
21+
{
22+
private final XmlMapper MAPPER = newMapper();
23+
24+
// Documents the current fail-fast behavior: 2D primitive array throws on
25+
// serialization (was silently producing the same output as 1D before fix).
26+
@Test
27+
public void test2DPrimitiveArrayFailsFast() throws Exception
28+
{
29+
try {
30+
MAPPER.writeValueAsString(new boolean[][] { { true }, { false } });
31+
fail("Should not pass: nested arrays must be rejected");
32+
} catch (JacksonException e) {
33+
verifyException(e, "does not support nested arrays");
34+
}
35+
}
36+
37+
// Same for nested Lists.
38+
@Test
39+
public void testNestedListFailsFast() throws Exception
40+
{
41+
List<List<String>> nested = Arrays.asList(
42+
Arrays.asList("a", "b"), Arrays.asList("c"));
43+
try {
44+
MAPPER.writeValueAsString(nested);
45+
fail("Should not pass: nested Collections must be rejected");
46+
} catch (JacksonException e) {
47+
verifyException(e, "does not support nested arrays");
48+
}
49+
}
50+
51+
// Disabling FAIL_ON_NESTED_ARRAYS restores legacy 2.x behavior: nested
52+
// dimensions are silently flattened (no exception thrown).
53+
@Test
54+
public void testLegacyFlatteningWhenFeatureDisabled() throws Exception
55+
{
56+
String xml = MAPPER.writer()
57+
.without(XmlWriteFeature.FAIL_ON_NESTED_ARRAYS)
58+
.writeValueAsString(new boolean[][] { { true }, { false } });
59+
assertEquals("<booleans><item>true</item><item>false</item></booleans>", xml);
60+
}
61+
62+
// Eventual goal: a 2D array should round-trip with proper nesting.
63+
// Currently fails (fail-fast above); annotation inverts pass/fail so this
64+
// entry tracks the unsupported-but-desired behavior.
65+
/*
66+
@JacksonTestFailureExpected
67+
@Test
68+
public void test2DArrayRoundTrip() throws Exception
69+
{
70+
boolean[][] input = new boolean[][] { { true }, { false } };
71+
String xml = MAPPER.writeValueAsString(input);
72+
boolean[][] result = MAPPER.readValue(xml, boolean[][].class);
73+
assertEquals(input.length, result.length);
74+
assertEquals(input[0][0], result[0][0]);
75+
assertEquals(input[1][0], result[1][0]);
76+
}
77+
*/
78+
}

0 commit comments

Comments
 (0)