From e35bf2dac23282a858df140a2fc1816578b529eb Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Thu, 30 Jul 2026 13:30:49 +0530 Subject: [PATCH] Honor `xsi:nil` in any attribute position and `xs:boolean` form --- .../dataformat/xml/deser/XmlTokenStream.java | 59 +++++++++++---- .../dataformat/xml/deser/XsiNilBasicTest.java | 75 +++++++++++++++++++ .../xml/deser/XsiSchemaLocationTest.java | 15 ++++ 3 files changed, 135 insertions(+), 14 deletions(-) diff --git a/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java b/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java index d52938e28..cbabade75 100644 --- a/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java +++ b/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java @@ -102,6 +102,16 @@ public class XmlTokenStream */ protected boolean _xsiNilFound; + /** + * Index of the {@code xsi:nil} attribute of the current START_ELEMENT, if + * one was found (and {@code xsi:nil} processing is enabled); -1 if none. + * Kept so that the marker attribute itself is not exposed as a regular + * property no matter which position it occurs in. + * + * @since 3.3 + */ + protected int _xsiNilIndex = -1; + /** * Flag set true if current event is {@code XML_TEXT} and there is START_ELEMENT * @@ -492,6 +502,11 @@ private final int _next() throws XMLStreamException //System.out.println(" XmlTokenStream._next(): Got xsi:nil, skipping element"); return _handleEndElement(); } + // [dataformat-xml#354]: the xsi:nil marker itself is never exposed as + // a property when xsi:nil processing is enabled + if (_nextAttributeIndex == _xsiNilIndex) { + ++_nextAttributeIndex; + } // [dataformat-xml#358]: Optionally skip XSI namespace attributes other // than "type" (handled by _decodeAttributeName) and "nil" (when xsi:nil // processing is disabled, it should be exposed as a regular attribute) @@ -746,27 +761,43 @@ private final int _initStartElement() throws XMLStreamException * @since 2.10 */ private final void _checkXsiAttributes() { - int count = _xmlReader.getAttributeCount(); + final int count = _xmlReader.getAttributeCount(); _attributeCount = count; - - // [dataformat-xml#354]: xsi:nil handling; at first only if first attribute - if (count >= 1) { - // [dataformat-xml#468]: may disable xsi:nil processing - if (_cfgProcessXsiNil - && "nil".equals(_xmlReader.getAttributeLocalName(0))) { - if (XSI_NAMESPACE.equals(_xmlReader.getAttributeNamespace(0))) { - // need to skip, regardless of value - _nextAttributeIndex = 1; - // but only mark as nil marker if enabled - _xsiNilFound = "true".equals(_xmlReader.getAttributeValue(0)); + _nextAttributeIndex = 0; + _xsiNilFound = false; + _xsiNilIndex = -1; + + // [dataformat-xml#354]: xsi:nil handling. Attribute order is not significant + // in XML so the marker has to be looked for in any position, not just first + // [dataformat-xml#468]: may disable xsi:nil processing + if (_cfgProcessXsiNil) { + for (int i = 0; i < count; ++i) { + if ("nil".equals(_xmlReader.getAttributeLocalName(i)) + && XSI_NAMESPACE.equals(_xmlReader.getAttributeNamespace(i))) { + // need to skip the attribute itself, regardless of value + _xsiNilIndex = i; + // but only mark as nil marker if value says so + _xsiNilFound = _isXsiNilTrue(_xmlReader.getAttributeValue(i)); //System.out.println(" XMLTokenStream._checkXsiAttributes(), _xsiNilFound: "+_xsiNilFound); return; } } } + } - _nextAttributeIndex = 0; - _xsiNilFound = false; + /** + * {@code xsi:nil} is of type {@code xs:boolean}, whose lexical space is + * "true", "false", "1" and "0", with surrounding white space collapsed. + * Comparison stays case-sensitive since {@code xs:boolean} is. + * + * @since 3.3 + */ + private static boolean _isXsiNilTrue(String value) { + if (value == null) { + return false; + } + final String v = value.trim(); + return "true".equals(v) || "1".equals(v); } /** diff --git a/src/test/java/tools/jackson/dataformat/xml/deser/XsiNilBasicTest.java b/src/test/java/tools/jackson/dataformat/xml/deser/XsiNilBasicTest.java index 5fe4a5969..f2a5f42cb 100644 --- a/src/test/java/tools/jackson/dataformat/xml/deser/XsiNilBasicTest.java +++ b/src/test/java/tools/jackson/dataformat/xml/deser/XsiNilBasicTest.java @@ -154,6 +154,81 @@ public void testDisableXsiNilLeafProcessing() throws Exception .readValue(DOC).toString()); } + // [dataformat-xml#354]: attribute order is not significant in XML, so the + // marker has to be honored wherever it occurs in the attribute list + @Test + public void testXsiNilAfterOtherAttribute() throws Exception + { + final ObjectReader r = MAPPER.readerFor(JsonNode.class); + + assertEquals(a2q("{'d':null}"), + r.readValue("").toString()); + assertEquals(a2q("{'d':null}"), + r.readValue("").toString()); + // and same for an element that has content, not just attributes + assertEquals(a2q("{'d':null}"), + r.readValue("0.25").toString()); + } + + @Test + public void testXsiNilAfterOtherAttributeForPojo() throws Exception + { + DoubleWrapper2 bean = MAPPER.readValue( +"\n" ++"\n" ++"0.25\n" ++"", + DoubleWrapper2.class); + assertNotNull(bean); + assertNull(bean.a); + assertEquals(Double.valueOf(0.25), bean.b); + } + + // Marker attribute itself must not be exposed as a property either, no matter + // its position; without this, "xsi:nil" leaks in as a property named "nil" + @Test + public void testXsiNilFalseAfterOtherAttributeNotExposed() throws Exception + { + final ObjectReader r = MAPPER.readerFor(JsonNode.class); + + assertEquals(a2q("{'d':{'id':'1','':'0.25'}}"), + r.readValue("0.25").toString()); + assertEquals(a2q("{'d':{'id':'1','':'0.25'}}"), + r.readValue("0.25").toString()); + } + + // `xsi:nil` is `xs:boolean`, so "1" is as much a nil marker as "true", and + // surrounding white space is collapsed before the value is read + @Test + public void testXsiNilBooleanLexicalForms() throws Exception + { + final ObjectReader r = MAPPER.readerFor(JsonNode.class); + + for (String nil : new String[] { "true", "1", " true ", "\n1\t" }) { + assertEquals(a2q("{'d':null}"), + r.readValue("0.25").toString(), + "xsi:nil='"+nil+"' should mark element as null"); + } + // ... but "false"/"0" do not, and neither does a non-boolean value + // ("xs:boolean" is case-sensitive, so "TRUE" is not valid) + for (String nonNil : new String[] { "false", "0", "TRUE", "yes" }) { + assertEquals(a2q("{'d':'0.25'}"), + r.readValue("0.25").toString(), + "xsi:nil='"+nonNil+"' should not mark element as null"); + } + } + + @Test + public void testXsiNilAfterOtherAttributeOnRoot() throws Exception + { + final ObjectReader r = MAPPER_NO_TRAILING_CHECK.readerFor(JsonNode.class); + + assertEquals("null", + r.readValue("").toString()); + assertEquals("null", + r.readValue("").toString()); + } + // [dataformat-xml#714]: trailing bogus `JsonToken.END_OBJECT` /* @Test diff --git a/src/test/java/tools/jackson/dataformat/xml/deser/XsiSchemaLocationTest.java b/src/test/java/tools/jackson/dataformat/xml/deser/XsiSchemaLocationTest.java index 4ad8d7276..b9b7d8fe6 100644 --- a/src/test/java/tools/jackson/dataformat/xml/deser/XsiSchemaLocationTest.java +++ b/src/test/java/tools/jackson/dataformat/xml/deser/XsiSchemaLocationTest.java @@ -73,6 +73,21 @@ public void testXsiSchemaLocationWithRegularAttributes() throws Exception assertEquals(42, result.count); } + // Skipping the "unknown" XSI attributes must not take xsi:nil with it: + // schema-annotated documents put xsi:schemaLocation first, leaving the + // nil marker in a later position + @Test + public void testXsiNilAfterXsiSchemaLocation() throws Exception + { + Dto result = MAPPER_SKIP.readValue( + "" + + "" + + "", + Dto.class); + assertNull(result.value); + } + // When feature is disabled (default), XSI attributes should be exposed // and bindable to POJO properties @Test