Skip to content

Commit cabe8fa

Browse files
authored
Fix #344: null for polymorphic element of Collection (#829)
1 parent 8dc929a commit cabe8fa

2 files changed

Lines changed: 152 additions & 1 deletion

File tree

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ public class FromXmlParser
110110
*/
111111
protected boolean _nextIsLeadingMixed;
112112

113+
/**
114+
* [dataformat-xml#344]: Flag set when a START_ELEMENT with xsi:nil is encountered
115+
* in the {@code _mayBeLeaf} path. When true, the subsequent END_ELEMENT should
116+
* produce {@code VALUE_NULL} instead of the empty-Object ({@code START_OBJECT}/{@code END_OBJECT})
117+
* pattern from [dataformat-xml#180].
118+
*
119+
* @since 3.2
120+
*/
121+
protected boolean _nextIsNullXsiNil;
122+
113123
/*
114124
/**********************************************************************
115125
/* Parsing state, parsed values
@@ -120,7 +130,7 @@ public class FromXmlParser
120130
* ByteArrayBuilder is needed if 'getBinaryValue' is called. If so,
121131
* we better reuse it for remainder of content.
122132
*/
123-
protected ByteArrayBuilder _byteArrayBuilder = null;
133+
protected ByteArrayBuilder _byteArrayBuilder;
124134

125135
/**
126136
* We will hold on to decoded binary data, for duration of
@@ -560,6 +570,10 @@ public JsonToken nextToken() throws JacksonException
560570
while (token == XmlTokenStream.XML_START_ELEMENT) {
561571
// If we thought we might get leaf, no such luck
562572
if (_mayBeLeaf) {
573+
// [dataformat-xml#344]: Record xsi:nil on the child element so that
574+
// the subsequent END_ELEMENT handler can produce VALUE_NULL instead of
575+
// the START_OBJECT/END_OBJECT empty-Object pattern from #180.
576+
_nextIsNullXsiNil = _xmlTokens.hasXsiNil();
563577
// leave _mayBeLeaf set, as we start a new context
564578
_nextToken = JsonToken.PROPERTY_NAME;
565579
_streamReadContext = _streamReadContext.createChildObjectContext(-1, -1);
@@ -568,7 +582,16 @@ public JsonToken nextToken() throws JacksonException
568582
if (_streamReadContext.inArray()) {
569583
// Yup: in array, so this element could be verified; but it won't be
570584
// reported anyway, and we need to process following event.
585+
// [dataformat-xml#344]: Check for xsi:nil BEFORE consuming the element;
586+
// if found, produce VALUE_NULL directly instead of falling through to
587+
// the #180 empty-Object handling (START_OBJECT/END_OBJECT) which breaks
588+
// polymorphic type resolution.
589+
final boolean xsiNil = _xmlTokens.hasXsiNil();
571590
token = _nextToken();
591+
if (xsiNil) {
592+
_streamReadContext.valueStarted();
593+
return _updateToken(JsonToken.VALUE_NULL);
594+
}
572595
_mayBeLeaf = true;
573596
continue;
574597
}
@@ -600,12 +623,22 @@ public JsonToken nextToken() throws JacksonException
600623
if (_mayBeLeaf) {
601624
_mayBeLeaf = false;
602625
if (_streamReadContext.inArray()) {
626+
// [dataformat-xml#344]: if the element had xsi:nil, produce
627+
// VALUE_NULL directly — polymorphic type resolvers cannot handle
628+
// the empty START_OBJECT/END_OBJECT from #180.
629+
if (_nextIsNullXsiNil) {
630+
_nextIsNullXsiNil = false;
631+
_streamReadContext.valueStarted();
632+
return _updateToken(JsonToken.VALUE_NULL);
633+
}
603634
// 06-Jan-2015, tatu: as per [dataformat-xml#180], need to
604635
// expose as empty Object, not null
605636
_nextToken = JsonToken.END_OBJECT;
606637
_streamReadContext = _streamReadContext.createChildObjectContext(-1, -1);
607638
return _updateToken(JsonToken.START_OBJECT);
608639
}
640+
// [dataformat-xml#344]: clear flag in non-array path too
641+
_nextIsNullXsiNil = false;
609642
// 07-Sep-2019, tatu: for [dataformat-xml#353], must NOT return second null
610643
if (_currToken != JsonToken.VALUE_NULL) {
611644
// 13-May-2020, tatu: [dataformat-xml#397]: advance `index`
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package tools.jackson.dataformat.xml.lists;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
import com.fasterxml.jackson.annotation.*;
9+
10+
import tools.jackson.databind.*;
11+
import tools.jackson.dataformat.xml.*;
12+
13+
import static org.junit.jupiter.api.Assertions.*;
14+
15+
/**
16+
* Test for [dataformat-xml#344]: Unable to deserialize null in polymorphic Collection.
17+
*<p>
18+
* With Jackson 3.x default of {@code WRITE_NULLS_AS_XSI_NIL = true},
19+
* null collection elements serialize as {@code <details xsi:nil="true"/>}.
20+
* The deserializer must recognize xsi:nil on array elements and produce
21+
* {@code VALUE_NULL} instead of {@code START_OBJECT}/{@code END_OBJECT}.
22+
*/
23+
public class PolymorphicListNullElement344Test extends XmlTestUtil
24+
{
25+
static class Master344 {
26+
public List<Detail344> details = new ArrayList<>();
27+
}
28+
29+
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "cl")
30+
static class Detail344 {
31+
public String value;
32+
33+
protected Detail344() { }
34+
public Detail344(String v) { value = v; }
35+
}
36+
37+
private final XmlMapper MAPPER = newMapper();
38+
39+
// [dataformat-xml#344]: null element in polymorphic list, round-trip
40+
@Test
41+
public void testNullInPolymorphicListRoundTrip() throws Exception
42+
{
43+
Master344 master = new Master344();
44+
master.details.add(new Detail344("first"));
45+
master.details.add(null);
46+
master.details.add(new Detail344("third"));
47+
48+
String xml = MAPPER.writeValueAsString(master);
49+
50+
Master344 result = MAPPER.readValue(xml, Master344.class);
51+
52+
assertNotNull(result);
53+
assertNotNull(result.details);
54+
assertEquals(3, result.details.size());
55+
assertNotNull(result.details.get(0));
56+
assertEquals("first", result.details.get(0).value);
57+
assertNull(result.details.get(1));
58+
assertNotNull(result.details.get(2));
59+
assertEquals("third", result.details.get(2).value);
60+
}
61+
62+
// [dataformat-xml#344]: null element in polymorphic list, deserialize from xsi:nil XML
63+
@Test
64+
public void testNullInPolymorphicListFromXsiNil() throws Exception
65+
{
66+
String xml = "<Master344>"
67+
+ "<details>"
68+
+ "<details xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:nil=\"true\"/>"
69+
+ "</details>"
70+
+ "</Master344>";
71+
72+
Master344 result = MAPPER.readValue(xml, Master344.class);
73+
74+
assertNotNull(result);
75+
assertNotNull(result.details);
76+
assertEquals(1, result.details.size());
77+
assertNull(result.details.get(0));
78+
}
79+
80+
// [dataformat-xml#344]: only null elements in polymorphic list
81+
@Test
82+
public void testOnlyNullsInPolymorphicList() throws Exception
83+
{
84+
Master344 master = new Master344();
85+
master.details.add(null);
86+
87+
String xml = MAPPER.writeValueAsString(master);
88+
Master344 result = MAPPER.readValue(xml, Master344.class);
89+
90+
assertNotNull(result);
91+
assertNotNull(result.details);
92+
assertEquals(1, result.details.size());
93+
assertNull(result.details.get(0));
94+
}
95+
96+
// [dataformat-xml#344]: multiple consecutive nulls in polymorphic list.
97+
// First null exercises _mayBeLeaf + END_ELEMENT path (fix site 1+3),
98+
// second null exercises inArray() path (fix site 2).
99+
@Test
100+
public void testMultipleNullsInPolymorphicList() throws Exception
101+
{
102+
Master344 master = new Master344();
103+
master.details.add(null);
104+
master.details.add(null);
105+
master.details.add(new Detail344("third"));
106+
107+
String xml = MAPPER.writeValueAsString(master);
108+
Master344 result = MAPPER.readValue(xml, Master344.class);
109+
110+
assertNotNull(result);
111+
assertNotNull(result.details);
112+
assertEquals(3, result.details.size());
113+
assertNull(result.details.get(0));
114+
assertNull(result.details.get(1));
115+
assertNotNull(result.details.get(2));
116+
assertEquals("third", result.details.get(2).value);
117+
}
118+
}

0 commit comments

Comments
 (0)