Skip to content

Commit addb6da

Browse files
committed
Merge branch '3.2' into 3.x
2 parents 4cd69b6 + bd84cb1 commit addb6da

7 files changed

Lines changed: 140 additions & 13 deletions

File tree

release-notes/CREDITS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,8 @@ Veit-Hendrik Schlenker (@vhschlenker)
147147
Charles Moulliard (@cmoulliard)
148148
* Reported #795: `HttpHeader` object (= <httpHeaders>) is not wrapped by the XML `<property>` tag
149149
(3.2.0)
150+
151+
Christian Beikov (@beikov)
152+
* Reported #871: `XmlMapper` regression in 3.2.0: `xsi:nil` collection element on
153+
unwrapped collection wrongly read as null collection
154+
(3.3.0)

release-notes/VERSION

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ Version: 3.x (for earlier see VERSION-2.x)
77

88
3.3.0 (not yet released)
99

10-
No changes since 3.2
10+
#871: `XmlMapper` regression in 3.2.0: `xsi:nil` collection element on unwrapped
11+
collection wrongly read as null collection (instead of collection with one
12+
null element); null unwrapped collection now omitted on write
13+
(reported by Christian B, @beikov)
14+
(fix by @cowtowncoder, w/ Claude code)
1115

1216
3.2.0 (08-Jun-2026)
1317

src/main/java/tools/jackson/dataformat/xml/deser/FromXmlParser.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -675,14 +675,13 @@ public JsonToken nextToken() throws JacksonException
675675
_streamReadContext.setCurrentName(name);
676676

677677
// Ok: virtual wrapping can be done by simply repeating current START_ELEMENT.
678-
// Couple of ways to do it; but start by making _xmlTokens replay the thing...
679678
if (_streamReadContext.shouldWrap(name)) {
680-
// [dataformat-xml#627]: But if xsi:nil="true" found, do NOT wrap —
681-
// let the normal xsi:nil handling produce VALUE_NULL for the property
682-
// instead of creating a list with one empty element.
683-
if (!_xmlTokens.hasXsiNil()) {
684-
_xmlTokens.repeatStartElement();
685-
}
679+
// [dataformat-xml#871]: Always repeat START_ELEMENT to form the
680+
// virtual array wrapper.
681+
// [dataformats-xml#627] skipped wrapping on xsi:nil, but that made
682+
// valid `<e xsi:nil="true"/>` collection items wrongly read as null
683+
// collections and silently dropped leading null elements.
684+
_xmlTokens.repeatStartElement();
686685
}
687686

688687
_mayBeLeaf = true;

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
7777

7878
// first things first: no wrapping?
7979
if (wrapperName == null || wrapperName == PropertyName.NO_NAME) {
80+
// [dataformat-xml#627]/[dataformat-xml#871]: For an unwrapped
81+
// Collection/array property, a null value must be omitted (as in the
82+
// wrapped case below) rather than written as an `xsi:nil` element --
83+
// otherwise it is indistinguishable, on read, from a collection holding
84+
// a single null element. Only applies to concrete indexed types (not the
85+
// dynamic Object-typed case) and only to plain BeanPropertyWriters.
86+
if (!dynamicWrapping && bpw.getClass() == BeanPropertyWriter.class) {
87+
beanProperties.set(i, new XmlNullSuppressingBeanPropertyWriter(bpw));
88+
}
8089
continue;
8190
}
8291
// no local name? Just double the wrapped name for wrapper
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package tools.jackson.dataformat.xml.ser;
2+
3+
import tools.jackson.core.JsonGenerator;
4+
import tools.jackson.databind.SerializationContext;
5+
import tools.jackson.databind.ser.BeanPropertyWriter;
6+
7+
/**
8+
* Property writer sub-class used for unwrapped (no wrapper element) Collection/array
9+
* properties. It suppresses output of {@code null} values entirely, so that a null
10+
* collection is simply omitted -- consistent with the wrapped-collection case handled
11+
* by {@link XmlBeanPropertyWriter} (which also omits null collections).
12+
*<p>
13+
* Without this, an unwrapped null collection would be written via the standard null
14+
* handling as an {@code xsi:nil} element (when {@code WRITE_NULLS_AS_XSI_NIL} is enabled,
15+
* which it is by default in 3.x). That output is indistinguishable, on read, from a
16+
* collection containing a single null element (see [dataformat-xml#871]), so for null
17+
* collections we omit the element instead.
18+
*
19+
* @since 3.2.1
20+
*/
21+
public class XmlNullSuppressingBeanPropertyWriter
22+
extends BeanPropertyWriter
23+
{
24+
public XmlNullSuppressingBeanPropertyWriter(BeanPropertyWriter wrapped)
25+
{
26+
super(wrapped);
27+
}
28+
29+
@Override
30+
public void serializeAsProperty(Object bean, JsonGenerator g, SerializationContext ctxt)
31+
throws Exception
32+
{
33+
// [dataformat-xml#627]/[dataformat-xml#871]: omit a null unwrapped collection
34+
// (rather than writing it as an `xsi:nil` element), matching wrapped behavior.
35+
if (get(bean) == null) {
36+
return;
37+
}
38+
super.serializeAsProperty(bean, g, ctxt);
39+
}
40+
41+
// NOTE: intentionally do NOT override `serializeAsElement()` (used for Array/tabular
42+
// shape, e.g. `@JsonFormat(shape = ARRAY)`): there, null entries are positional and
43+
// must be written (as `null`), not suppressed -- otherwise following entries would
44+
// shift. Only the Object-shape `serializeAsProperty()` path suppresses nulls here.
45+
}

src/test/java/tools/jackson/dataformat/xml/lists/Issue627NullListTest.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ public void testNullListRoundTripNoWrapper() throws Exception
4242
assertNull(a.getChildren());
4343

4444
String xml = mapper.writeValueAsString(a);
45-
assertTrue(xml.contains("xsi:nil=\"true\""),
46-
"Null list should serialize with xsi:nil");
45+
// [dataformat-xml#871]: a null unwrapped collection is omitted (not written as
46+
// an `xsi:nil` element, which would be ambiguous with a single null element);
47+
// consistent with the wrapped-collection case.
48+
assertFalse(xml.contains("children"),
49+
"Null unwrapped list should be omitted, was: " + xml);
4750

4851
Parent b = mapper.readValue(xml, Parent.class);
4952

@@ -86,7 +89,9 @@ public void testNonNullListRoundTripNoWrapper() throws Exception
8689
assertEquals("test", b.getChildren().get(0).getName());
8790
}
8891

89-
// Verify deserializing explicit xsi:nil on unwrapped list property
92+
// [dataformat-xml#871]: an explicit xsi:nil on an unwrapped collection element
93+
// denotes a single null *element* within the collection -- NOT a null collection.
94+
// (A null collection is instead omitted on serialization; see round-trip tests above.)
9095
@Test
9196
public void testXsiNilUnwrappedListDeser() throws Exception
9297
{
@@ -97,7 +102,9 @@ public void testXsiNilUnwrappedListDeser() throws Exception
97102
String xml = "<Parent><children xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:nil=\"true\"/></Parent>";
98103
Parent b = mapper.readValue(xml, Parent.class);
99104

100-
assertNull(b.getChildren(),
101-
"xsi:nil on unwrapped list element should produce null list");
105+
assertNotNull(b.getChildren(),
106+
"xsi:nil on unwrapped list element should produce a list with one null element");
107+
assertEquals(1, b.getChildren().size());
108+
assertNull(b.getChildren().get(0));
102109
}
103110
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package tools.jackson.dataformat.xml.lists;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collection;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
import com.fasterxml.jackson.annotation.JsonRootName;
9+
10+
import tools.jackson.dataformat.xml.XmlMapper;
11+
import tools.jackson.dataformat.xml.XmlTestUtil;
12+
import tools.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
13+
import tools.jackson.dataformat.xml.annotation.JacksonXmlProperty;
14+
15+
import static org.junit.jupiter.api.Assertions.*;
16+
17+
/**
18+
* Test for [dataformat-xml#871]: an `xsi:nil="true"` collection element on an unwrapped
19+
* collection must read as a single null *element* within the collection, not as a null
20+
* collection (regression from the original #627 attempt).
21+
*/
22+
public class Issue871NullElementTest extends XmlTestUtil
23+
{
24+
@JsonRootName(value = "Collection")
25+
static class CollectionWrapper {
26+
@JacksonXmlElementWrapper(useWrapping = false)
27+
@JacksonXmlProperty(localName = "e")
28+
public Collection<String> value;
29+
30+
public CollectionWrapper() { this.value = new ArrayList<>(); }
31+
}
32+
33+
private final XmlMapper MAPPER = new XmlMapper();
34+
35+
private static final String NS = "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"";
36+
37+
@Test
38+
public void testSingleNullElement() throws Exception
39+
{
40+
String xml = "<Collection><e " + NS + " xsi:nil=\"true\"/></Collection>";
41+
CollectionWrapper cw = MAPPER.readValue(xml, CollectionWrapper.class);
42+
43+
assertNotNull(cw.value);
44+
assertEquals(1, cw.value.size());
45+
assertNull(cw.value.iterator().next());
46+
}
47+
48+
@Test
49+
public void testLeadingNullElementNotDropped() throws Exception
50+
{
51+
String xml = "<Collection><e " + NS + " xsi:nil=\"true\"/><e>x</e></Collection>";
52+
CollectionWrapper cw = MAPPER.readValue(xml, CollectionWrapper.class);
53+
54+
assertNotNull(cw.value);
55+
assertEquals(2, cw.value.size());
56+
assertEquals(java.util.Arrays.asList(null, "x"), new ArrayList<>(cw.value));
57+
}
58+
}

0 commit comments

Comments
 (0)